// 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