CompactUnitConversionCache.cs 11 KB

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