// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik // // Inspector Gadgets // https://kybernetik.com.au/animancer // Copyright 2017-2024 Kybernetik // #if UNITY_EDITOR && UNITY_IMGUI using Animancer.Editor; using System; using System.Collections.Generic; using System.Globalization; using UnityEditor; namespace Animancer.Units.Editor //namespace InspectorGadgets.Editor { /// [Editor-Only] /// A system for formatting floats as strings that fit into a limited area and storing the results so they can be /// reused to minimise the need for garbage collection, particularly for string construction. /// /// /// /// This system only affects the display value. Once you select a field, it shows its actual value. /// /// Example: /// With "x" as the suffix: /// /// 1.111111 could instead show 1.111~x. /// 0.00001234567 would normally show 1.234567e-05, but with this it instead shows 0~x /// because very small values generally aren't useful. /// 99999999 shows 1e+08x because very large values are already approximations and trying to /// format them correctly would be very difficult. /// /// /// /// https://kybernetik.com.au/animancer/api/Animancer.Units.Editor/CompactUnitConversionCache /// https://kybernetik.com.au/inspector-gadgets/api/InspectorGadgets.Editor/CompactUnitConversionCache /// public class CompactUnitConversionCache { /************************************************************************************************************************/ /// Should the fields show approximations if the value is too long for the GUI? public static bool ShowApproximations => AnimancerSettingsGroup.Instance.showApproximations; // => PropertyDrawers.TransformPropertyDrawer.ShowApproximations; /************************************************************************************************************************/ /// The suffix added to the end of each value. public readonly string Suffix; /// The with a ~ before it to indicate an approximation. public readonly string ApproximateSuffix; /// The value 0 with the . public readonly string ConvertedZero; /// The value 0 with the . public readonly string ConvertedSmallPositive; /// The value -0 with the . public readonly string ConvertedSmallNegative; /// The pixel width of the when drawn by . public float _SuffixWidth; /// The caches for each character count. /// this[x] is a cache that outputs strings with x characters. private readonly List> Caches = new(); /************************************************************************************************************************/ /// Strings mapped to the width they would require for a . private static ConversionCache _WidthCache; /// Padding around the text in a . public static float _FieldPadding; /// The pixel width of the ~ character when drawn by . public static float _ApproximateSymbolWidth; /// The character(s) used to separate decimal values in the current OS language. public static string _DecimalSeparator; /// Values smaller than this become 0~ or -0~. public const float SmallExponentialThreshold = 0.0001f; /// Values larger than this can't be approximated. public const float LargeExponentialThreshold = 9999999f; /************************************************************************************************************************/ /// Creates a new . public CompactUnitConversionCache(string suffix) { Suffix = suffix; ApproximateSuffix = "~" + Suffix; ConvertedZero = "0" + Suffix; ConvertedSmallPositive = "0" + ApproximateSuffix; ConvertedSmallNegative = "-0" + ApproximateSuffix; } /************************************************************************************************************************/ /// /// Returns a cached string representing the `value` trimmed to fit within the `width` (if necessary) and with /// the added on the end. /// public string Convert(float value, float width) { if (value == 0) return ConvertedZero; if (!ShowApproximations) return GetCache(0).Convert(value); if (value < SmallExponentialThreshold && value > -SmallExponentialThreshold) return value > 0 ? ConvertedSmallPositive : ConvertedSmallNegative; var index = CalculateCacheIndex(value, width); return GetCache(index).Convert(value); } /************************************************************************************************************************/ /// Calculate the index of the cache to use for the given parameters. private int CalculateCacheIndex(float value, float width) { //if (value > LargeExponentialThreshold || // value < -LargeExponentialThreshold) // return 0; var valueString = value.ToStringCached(); // It the approximated string wouldn't be shorter than the original, don't approximate. if (valueString.Length < 2 + ApproximateSuffix.Length) return 0; if (_SuffixWidth == 0) { if (_WidthCache == null) { _WidthCache = ConversionCache.CreateWidthCache(EditorStyles.numberField); _FieldPadding = EditorStyles.numberField.padding.horizontal; _ApproximateSymbolWidth = _WidthCache.Convert("~") - _FieldPadding; } if (!string.IsNullOrWhiteSpace(Suffix)) _SuffixWidth = _WidthCache.Convert(Suffix); } // If the field is wide enough to fit the full value, don't approximate. width -= _FieldPadding + _ApproximateSymbolWidth * 0.75f; var valueWidth = _WidthCache.Convert(valueString) + _SuffixWidth; if (valueWidth <= width) return 0; // If the number of allowed characters would include the full value, don't approximate. var suffixedLength = valueString.Length + Suffix.Length; var allowedCharacters = (int)(suffixedLength * width / valueWidth); if (allowedCharacters + 2 >= suffixedLength) return 0; return allowedCharacters; } /************************************************************************************************************************/ /// Creates and returns a cache for the specified `characterCount`. private ConversionCache GetCache(int characterCount) { while (Caches.Count <= characterCount) Caches.Add(null); var cache = Caches[characterCount]; if (cache == null) { if (characterCount == 0) { cache = new((value) => { return value.ToStringCached() + Suffix; }); } else { cache = new((value) => { var valueString = value.ToStringCached(); if (value > LargeExponentialThreshold || value < -LargeExponentialThreshold) goto IsExponential; _DecimalSeparator ??= CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator; var decimalIndex = valueString.IndexOf(_DecimalSeparator); if (decimalIndex < 0 || decimalIndex > characterCount) goto IsExponential; // Not exponential. return valueString[..characterCount] + ApproximateSuffix; IsExponential: var digits = Math.Max(0, characterCount - ApproximateSuffix.Length - 1); var format = GetExponentialFormat(digits); valueString = value.ToString(format); TrimExponential(ref valueString); return valueString + Suffix; }); } Caches[characterCount] = cache; } return cache; } /************************************************************************************************************************/ private static List _ExponentialFormats; /// Returns a format string to include the specified number of `digits` in an exponential number. public static string GetExponentialFormat(int digits) { _ExponentialFormats ??= new(); while (_ExponentialFormats.Count <= digits) _ExponentialFormats.Add("g" + _ExponentialFormats.Count); return _ExponentialFormats[digits]; } /************************************************************************************************************************/ private static void TrimExponential(ref string valueString) { var length = valueString.Length; if (length <= 4 || valueString[length - 4] != 'e' || valueString[length - 2] != '0') return; valueString = valueString[..(length - 2)] + valueString[length - 1]; } /************************************************************************************************************************/ } } #endif