Net.Like.Xue.Tokyo/Assets/Plugins/Animancer/Internal/Editor/CompactUnitConversionCache.cs

244 lines
10 KiB
C#

// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Globalization;
using UnityEditor;
namespace Animancer.Editor
{
/// <summary>[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.
/// </summary>
/// <example>
/// With <c>"x"</c> as the suffix:
/// <list type="bullet">
/// <item><c>1.111111</c> could instead show <c>1.111~x</c>.</item>
/// <item><c>0.00001234567</c> would normally show <c>1.234567e-05</c>, but with this it instead shows <c>0~x</c>
/// because very small values generally aren't useful.</item>
/// <item><c>99999999</c> shows <c>1e+08x</c> because very large values are already approximations and trying to
/// format them correctly would be very difficult.</item>
/// </list>
/// This system only affects the display value. Once you select a field, it shows its actual value.
/// </example>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/CompactUnitConversionCache
///
public class CompactUnitConversionCache
{
/************************************************************************************************************************/
/// <summary>The suffix added to the end of each value.</summary>
public readonly string Suffix;
/// <summary>The <see cref="Suffix"/> with a <c>~</c> before it to indicate an approximation.</summary>
public readonly string ApproximateSuffix;
/// <summary>The value <c>0</c> with the <see cref="Suffix"/>.</summary>
public readonly string ConvertedZero;
/// <summary>The value <c>0</c> with the <see cref="ApproximateSuffix"/>.</summary>
public readonly string ConvertedSmallPositive;
/// <summary>The value <c>-0</c> with the <see cref="ApproximateSuffix"/>.</summary>
public readonly string ConvertedSmallNegative;
/// <summary>The pixel width of the <see cref="Suffix"/> when drawn by <see cref="EditorStyles.numberField"/>.</summary>
public float _SuffixWidth;
/// <summary>The caches for each character count.</summary>
/// <remarks><c>this[x]</c> is a cache that outputs strings with <c>x</c> characters.</remarks>
private List<ConversionCache<float, string>>
Caches = new List<ConversionCache<float, string>>();
/************************************************************************************************************************/
/// <summary>Strings mapped to the width they would require for a <see cref="EditorStyles.numberField"/>.</summary>
private static ConversionCache<string, float> _WidthCache;
/// <summary>Padding around the text in a <see cref="EditorStyles.numberField"/>.</summary>
public static float _FieldPadding;
/// <summary>The pixel width of the <c>~</c> character when drawn by <see cref="EditorStyles.numberField"/>.</summary>
public static float _ApproximateSymbolWidth;
/// <summary>The character(s) used to separate decimal values in the current OS language.</summary>
public static string _DecimalSeparator;
/// <summary>Values smaller than this become <c>0~</c> or <c>-0~</c>.</summary>
public const float
SmallExponentialThreshold = 0.0001f;
/// <summary>Values larger than this can't be approximated.</summary>
public const float
LargeExponentialThreshold = 9999999f;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="CompactUnitConversionCache"/>.</summary>
public CompactUnitConversionCache(string suffix)
{
Suffix = suffix;
ApproximateSuffix = "~" + suffix;
ConvertedZero = "0" + Suffix;
ConvertedSmallPositive = "0" + ApproximateSuffix;
ConvertedSmallNegative = "-0" + ApproximateSuffix;
}
/************************************************************************************************************************/
/// <summary>
/// Returns a cached string representing the `value` trimmed to fit within the `width` (if necessary) and with
/// the <see cref="Suffix"/> added on the end.
/// </summary>
public string Convert(float value, float width)
{
if (value == 0)
return ConvertedZero;
if (!AnimancerSettings.AnimationTimeFields.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);
}
/************************************************************************************************************************/
/// <summary>Calculate the index of the cache to use for the given parameters.</summary>
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 = AnimancerGUI.CreateWidthCache(EditorStyles.numberField);
_FieldPadding = EditorStyles.numberField.padding.horizontal;
_ApproximateSymbolWidth = _WidthCache.Convert("~") - _FieldPadding;
}
_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;
}
/************************************************************************************************************************/
/// <summary>Creates and returns a cache for the specified `characterCount`.</summary>
private ConversionCache<float, string> GetCache(int characterCount)
{
while (Caches.Count <= characterCount)
Caches.Add(null);
var cache = Caches[characterCount];
if (cache == null)
{
if (characterCount == 0)
{
cache = new ConversionCache<float, string>((value) =>
{
return value.ToStringCached() + Suffix;
});
}
else
{
cache = new ConversionCache<float, string>((value) =>
{
var valueString = value.ToStringCached();
if (value > LargeExponentialThreshold ||
value < -LargeExponentialThreshold)
goto IsExponential;
if (_DecimalSeparator == null)
_DecimalSeparator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
var decimalIndex = valueString.IndexOf(_DecimalSeparator);
if (decimalIndex < 0 || decimalIndex > characterCount)
goto IsExponential;
// Not exponential.
return valueString.Substring(0, 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<string> _ExponentialFormats;
/// <summary>Returns a format string to include the specified number of `digits` in an exponential number.</summary>
public static string GetExponentialFormat(int digits)
{
if (_ExponentialFormats == null)
_ExponentialFormats = new List<string>();
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.Substring(0, length - 2) +
valueString[length - 1];
}
/************************************************************************************************************************/
}
}
#endif