CompactUnitConversionCache.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2022 Kybernetik //
  2. #if UNITY_EDITOR
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Globalization;
  6. using UnityEditor;
  7. namespace Animancer.Editor
  8. {
  9. /// <summary>[Editor-Only]
  10. /// A system for formatting floats as strings that fit into a limited area and storing the results so they can be
  11. /// reused to minimise the need for garbage collection, particularly for string construction.
  12. /// </summary>
  13. /// <example>
  14. /// With <c>"x"</c> as the suffix:
  15. /// <list type="bullet">
  16. /// <item><c>1.111111</c> could instead show <c>1.111~x</c>.</item>
  17. /// <item><c>0.00001234567</c> would normally show <c>1.234567e-05</c>, but with this it instead shows <c>0~x</c>
  18. /// because very small values generally aren't useful.</item>
  19. /// <item><c>99999999</c> shows <c>1e+08x</c> because very large values are already approximations and trying to
  20. /// format them correctly would be very difficult.</item>
  21. /// </list>
  22. /// This system only affects the display value. Once you select a field, it shows its actual value.
  23. /// </example>
  24. /// https://kybernetik.com.au/animancer/api/Animancer.Editor/CompactUnitConversionCache
  25. ///
  26. public class CompactUnitConversionCache
  27. {
  28. /************************************************************************************************************************/
  29. /// <summary>The suffix added to the end of each value.</summary>
  30. public readonly string Suffix;
  31. /// <summary>The <see cref="Suffix"/> with a <c>~</c> before it to indicate an approximation.</summary>
  32. public readonly string ApproximateSuffix;
  33. /// <summary>The value <c>0</c> with the <see cref="Suffix"/>.</summary>
  34. public readonly string ConvertedZero;
  35. /// <summary>The value <c>0</c> with the <see cref="ApproximateSuffix"/>.</summary>
  36. public readonly string ConvertedSmallPositive;
  37. /// <summary>The value <c>-0</c> with the <see cref="ApproximateSuffix"/>.</summary>
  38. public readonly string ConvertedSmallNegative;
  39. /// <summary>The pixel width of the <see cref="Suffix"/> when drawn by <see cref="EditorStyles.numberField"/>.</summary>
  40. public float _SuffixWidth;
  41. /// <summary>The caches for each character count.</summary>
  42. /// <remarks><c>this[x]</c> is a cache that outputs strings with <c>x</c> characters.</remarks>
  43. private List<ConversionCache<float, string>>
  44. Caches = new List<ConversionCache<float, string>>();
  45. /************************************************************************************************************************/
  46. /// <summary>Strings mapped to the width they would require for a <see cref="EditorStyles.numberField"/>.</summary>
  47. private static ConversionCache<string, float> _WidthCache;
  48. /// <summary>Padding around the text in a <see cref="EditorStyles.numberField"/>.</summary>
  49. public static float _FieldPadding;
  50. /// <summary>The pixel width of the <c>~</c> character when drawn by <see cref="EditorStyles.numberField"/>.</summary>
  51. public static float _ApproximateSymbolWidth;
  52. /// <summary>The character(s) used to separate decimal values in the current OS language.</summary>
  53. public static string _DecimalSeparator;
  54. /// <summary>Values smaller than this become <c>0~</c> or <c>-0~</c>.</summary>
  55. public const float
  56. SmallExponentialThreshold = 0.0001f;
  57. /// <summary>Values larger than this can't be approximated.</summary>
  58. public const float
  59. LargeExponentialThreshold = 9999999f;
  60. /************************************************************************************************************************/
  61. /// <summary>Creates a new <see cref="CompactUnitConversionCache"/>.</summary>
  62. public CompactUnitConversionCache(string suffix)
  63. {
  64. Suffix = suffix;
  65. ApproximateSuffix = "~" + suffix;
  66. ConvertedZero = "0" + Suffix;
  67. ConvertedSmallPositive = "0" + ApproximateSuffix;
  68. ConvertedSmallNegative = "-0" + ApproximateSuffix;
  69. }
  70. /************************************************************************************************************************/
  71. /// <summary>
  72. /// Returns a cached string representing the `value` trimmed to fit within the `width` (if necessary) and with
  73. /// the <see cref="Suffix"/> added on the end.
  74. /// </summary>
  75. public string Convert(float value, float width)
  76. {
  77. if (value == 0)
  78. return ConvertedZero;
  79. if (!AnimancerSettings.AnimationTimeFields.showApproximations)
  80. return GetCache(0).Convert(value);
  81. if (value < SmallExponentialThreshold &&
  82. value > -SmallExponentialThreshold)
  83. return value > 0 ? ConvertedSmallPositive : ConvertedSmallNegative;
  84. var index = CalculateCacheIndex(value, width);
  85. return GetCache(index).Convert(value);
  86. }
  87. /************************************************************************************************************************/
  88. /// <summary>Calculate the index of the cache to use for the given parameters.</summary>
  89. private int CalculateCacheIndex(float value, float width)
  90. {
  91. //if (value > LargeExponentialThreshold ||
  92. // value < -LargeExponentialThreshold)
  93. // return 0;
  94. var valueString = value.ToStringCached();
  95. // It the approximated string wouldn't be shorter than the original, don't approximate.
  96. if (valueString.Length < 2 + ApproximateSuffix.Length)
  97. return 0;
  98. if (_SuffixWidth == 0)
  99. {
  100. if (_WidthCache == null)
  101. {
  102. _WidthCache = AnimancerGUI.CreateWidthCache(EditorStyles.numberField);
  103. _FieldPadding = EditorStyles.numberField.padding.horizontal;
  104. _ApproximateSymbolWidth = _WidthCache.Convert("~") - _FieldPadding;
  105. }
  106. _SuffixWidth = _WidthCache.Convert(Suffix);
  107. }
  108. // If the field is wide enough to fit the full value, don't approximate.
  109. width -= _FieldPadding + _ApproximateSymbolWidth * 0.75f;
  110. var valueWidth = _WidthCache.Convert(valueString) + _SuffixWidth;
  111. if (valueWidth <= width)
  112. return 0;
  113. // If the number of allowed characters would include the full value, don't approximate.
  114. var suffixedLength = valueString.Length + Suffix.Length;
  115. var allowedCharacters = (int)(suffixedLength * width / valueWidth);
  116. if (allowedCharacters + 2 >= suffixedLength)
  117. return 0;
  118. return allowedCharacters;
  119. }
  120. /************************************************************************************************************************/
  121. /// <summary>Creates and returns a cache for the specified `characterCount`.</summary>
  122. private ConversionCache<float, string> GetCache(int characterCount)
  123. {
  124. while (Caches.Count <= characterCount)
  125. Caches.Add(null);
  126. var cache = Caches[characterCount];
  127. if (cache == null)
  128. {
  129. if (characterCount == 0)
  130. {
  131. cache = new ConversionCache<float, string>((value) =>
  132. {
  133. return value.ToStringCached() + Suffix;
  134. });
  135. }
  136. else
  137. {
  138. cache = new ConversionCache<float, string>((value) =>
  139. {
  140. var valueString = value.ToStringCached();
  141. if (value > LargeExponentialThreshold ||
  142. value < -LargeExponentialThreshold)
  143. goto IsExponential;
  144. if (_DecimalSeparator == null)
  145. _DecimalSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
  146. var decimalIndex = valueString.IndexOf(_DecimalSeparator);
  147. if (decimalIndex < 0 || decimalIndex > characterCount)
  148. goto IsExponential;
  149. // Not exponential.
  150. return valueString.Substring(0, characterCount) + ApproximateSuffix;
  151. IsExponential:
  152. var digits = Math.Max(0, characterCount - ApproximateSuffix.Length - 1);
  153. var format = GetExponentialFormat(digits);
  154. valueString = value.ToString(format);
  155. TrimExponential(ref valueString);
  156. return valueString + Suffix;
  157. });
  158. }
  159. Caches[characterCount] = cache;
  160. }
  161. return cache;
  162. }
  163. /************************************************************************************************************************/
  164. private static List<string> _ExponentialFormats;
  165. /// <summary>Returns a format string to include the specified number of `digits` in an exponential number.</summary>
  166. public static string GetExponentialFormat(int digits)
  167. {
  168. if (_ExponentialFormats == null)
  169. _ExponentialFormats = new List<string>();
  170. while (_ExponentialFormats.Count <= digits)
  171. _ExponentialFormats.Add("g" + _ExponentialFormats.Count);
  172. return _ExponentialFormats[digits];
  173. }
  174. /************************************************************************************************************************/
  175. private static void TrimExponential(ref string valueString)
  176. {
  177. var length = valueString.Length;
  178. if (length <= 4 ||
  179. valueString[length - 4] != 'e' ||
  180. valueString[length - 2] != '0')
  181. return;
  182. valueString =
  183. valueString.Substring(0, length - 2) +
  184. valueString[length - 1];
  185. }
  186. /************************************************************************************************************************/
  187. }
  188. }
  189. #endif