chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 9e5936589d9791a4288596c4b383b592
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,54 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] Caches <see cref="AnimationClip.events"/> to reduce garbage allocations.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimationEventCache
///
public static class AnimationEventCache
{
/************************************************************************************************************************/
private static readonly ConditionalWeakTable<AnimationClip, AnimationEvent[]>
ClipToEvents = new();
/************************************************************************************************************************/
/// <summary>
/// Returns the <see cref="AnimationClip.events"/> and caches the result to avoid allocating more memory with
/// each subsequent call.
/// </summary>
public static AnimationEvent[] GetCachedEvents(this AnimationClip clip)
{
if (!ClipToEvents.TryGetValue(clip, out var events))
{
events = clip.events;
ClipToEvents.Add(clip, events);
}
return events;
}
/************************************************************************************************************************/
/// <summary>Clears the cache.</summary>
public static void Clear()
=> ClipToEvents.Clear();
/************************************************************************************************************************/
/// <summary>Removes the `clip` from the cache so its events will be retrieved again next time.</summary>
public static void Remove(AnimationClip clip)
=> ClipToEvents.Remove(clip);
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: bb828e75ad28a6441b43f561f568f98d
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,140 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] A cache to optimize repeated attribute access.</summary>
/// <remarks>
/// If <typeparamref name="TAttribute"/> implements <see cref="IInitializable{T}"/> for <see cref="MemberInfo"/>,
/// its <see cref="IInitializable{T}.Initialize(T)"/> method will be called automatically.
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AttributeCache_1
public static class AttributeCache<TAttribute>
where TAttribute : class
{
/************************************************************************************************************************/
private static readonly Dictionary<MemberInfo, TAttribute>
MemberToAttribute = new();
/************************************************************************************************************************/
/// <summary>
/// Returns the <typeparamref name="TAttribute"/> attribute on the specified `member` (if there is one).
/// </summary>
public static TAttribute GetAttribute(MemberInfo member)
{
if (!MemberToAttribute.TryGetValue(member, out var attribute))
{
try
{
attribute = member.GetAttribute<TAttribute>();
if (attribute is IInitializable<MemberInfo> initializable)
initializable.Initialize(member);
}
catch (Exception exception)
{
Debug.LogException(exception);
attribute = null;
}
MemberToAttribute.Add(member, attribute);
}
return attribute;
}
/************************************************************************************************************************/
/// <summary>
/// Returns the <typeparamref name="TAttribute"/> attribute (if any)
/// on the specified `type` or its <see cref="Type.BaseType"/> (recursively).
/// </summary>
public static TAttribute GetAttribute(Type type)
{
if (type == null)
return null;
var attribute = GetAttribute((MemberInfo)type);
if (attribute != null)
return attribute;
return MemberToAttribute[type] = GetAttribute(type.BaseType);
}
/************************************************************************************************************************/
/// <summary>
/// Returns the <typeparamref name="TAttribute"/> attribute on the specified `field` or its
/// <see cref="FieldInfo.FieldType"/> or <see cref="MemberInfo.DeclaringType"/>.
/// </summary>
public static TAttribute FindAttribute(FieldInfo field)
{
var attribute = GetAttribute(field);
if (attribute != null)
return attribute;
attribute = GetAttribute(field.FieldType);
if (attribute != null)
return MemberToAttribute[field] = attribute;
attribute = GetAttribute(field.DeclaringType);
if (attribute != null)
return MemberToAttribute[field] = attribute;
return attribute;
}
/************************************************************************************************************************/
/// <summary>[Editor-Only]
/// Returns the <typeparamref name="TAttribute"/> attribute on the underlying field
/// of the `property` or its <see cref="FieldInfo.FieldType"/> or
/// <see cref="MemberInfo.DeclaringType"/> or any of the parent properties
/// or the type of the <see cref="SerializedObject.targetObject"/>.
/// </summary>
public static TAttribute FindAttribute(SerializedProperty property)
{
var accessor = property.GetAccessor();
while (accessor != null)
{
var field = accessor.GetField(property);
var attribute = GetAttribute(field);
if (attribute != null)
return attribute;
var value = accessor.GetValue(property);
if (value != null)
{
attribute = GetAttribute(value.GetType());
if (attribute != null)
return attribute;
}
accessor = accessor.Parent;
}
// If none of the fields of types they are declared in have names, try the actual type of the target.
{
var attribute = GetAttribute(property.serializedObject.targetObject.GetType());
if (attribute != null)
return attribute;
}
return null;
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: ea23cad192fdb024b83eb42c328bc444
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,263 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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
{
/// <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>
///
/// <remarks>
/// This system only affects the display value. Once you select a field, it shows its actual value.
/// <para></para>
/// <strong>Example:</strong>
/// 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>
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer.Units.Editor/CompactUnitConversionCache
/// https://kybernetik.com.au/inspector-gadgets/api/InspectorGadgets.Editor/CompactUnitConversionCache
///
public class CompactUnitConversionCache
{
/************************************************************************************************************************/
/// <summary>Should the fields show approximations if the value is too long for the GUI?</summary>
public static bool ShowApproximations
=> AnimancerSettingsGroup<AnimationTimeAttributeSettings>.Instance.showApproximations;
// => PropertyDrawers.TransformPropertyDrawer.ShowApproximations;
/************************************************************************************************************************/
/// <summary>The suffix added to the end of each value.</summary>
public string Suffix;
/// <summary>The <see cref="Suffix"/> with a <c>~</c> before it to indicate an approximation.</summary>
public string ApproximateSuffix;
/// <summary>The value <c>0</c> with the <see cref="Suffix"/>.</summary>
public string ConvertedZero;
/// <summary>The value <c>0</c> with the <see cref="ApproximateSuffix"/>.</summary>
public string ConvertedSmallPositive;
/// <summary>The value <c>-0</c> with the <see cref="ApproximateSuffix"/>.</summary>
public string ConvertedSmallNegative;
/// <summary>The string to return when converting <see cref="float.NaN"/>.</summary>
public string ConvertedNaN;
/// <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 readonly List<ConversionCache<float, string>>
Caches = new();
/************************************************************************************************************************/
/// <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;
ConvertedNaN = "NaN" + 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;
else if (float.IsNaN(value))
return ConvertedNaN;
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);
}
/************************************************************************************************************************/
/// <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 = 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;
}
/************************************************************************************************************************/
/// <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((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<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)
{
_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

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: b282ce649e44335499d21e0472c670e1
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,137 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
// Inspector Gadgets // https://kybernetik.com.au/inspector-gadgets // Copyright 2017-2024 Kybernetik //
#if UNITY_EDITOR
//#define LOG_CONVERSION_CACHE
using System;
using System.Collections.Generic;
using UnityEngine;
// Shared File Last Modified: 2023-09-02
namespace Animancer.Editor
//namespace InspectorGadgets.Editor
{
/// <summary>[Editor-Only]
/// A simple system for converting objects and storing the results so they can be reused to minimise the need for
/// garbage collection, particularly for string construction.
/// </summary>
/// <remarks>This class doesn't use any Editor-Only functionality, but it's unlikely to be useful at runtime.</remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ConversionCache_2
/// https://kybernetik.com.au/inspector-gadgets/api/InspectorGadgets.Editor/ConversionCache_2
///
public class ConversionCache<TKey, TValue>
{
/************************************************************************************************************************/
private class CachedValue
{
public int lastFrameAccessed;
public TValue value;
}
/************************************************************************************************************************/
private readonly Dictionary<TKey, CachedValue>
Cache = new();
private readonly List<TKey>
Keys = new();
private readonly Func<TKey, TValue>
Converter;
private int _LastCleanupFrame;
/************************************************************************************************************************/
/// <summary>
/// Creates a new <see cref="ConversionCache{TKey, TValue}"/> which uses the specified delegate to convert values.
/// </summary>
public ConversionCache(Func<TKey, TValue> converter)
=> Converter = converter;
/************************************************************************************************************************/
/// <summary>
/// If a value has already been cached for the specified `key`, return it. Otherwise create a new one using
/// the delegate provided in the constructor and cache it.
/// <para></para>
/// If the `key` is <c>null</c>, this method returns the default <typeparamref name="TValue"/>.
/// </summary>
/// <remarks>This method also periodically removes values that have not been used recently.</remarks>
public TValue Convert(TKey key)
{
if (key == null)
return default;
CachedValue cached;
// The next time a value is retrieved after at least 100 frames, clear out any old ones.
var frame = Time.frameCount;
if (_LastCleanupFrame + 100 < frame)
{
for (int i = Keys.Count - 1; i >= 0; i--)
{
var checkKey = Keys[i];
if (!Cache.TryGetValue(checkKey, out cached) ||
cached.lastFrameAccessed <= _LastCleanupFrame)
{
Cache.Remove(checkKey);
Keys.RemoveAt(i);
}
}
_LastCleanupFrame = frame;
}
if (!Cache.TryGetValue(key, out cached))
{
Cache.Add(key, cached = new() { value = Converter(key) });
Keys.Add(key);
}
cached.lastFrameAccessed = frame;
return cached.value;
}
/************************************************************************************************************************/
}
/// <summary>[Editor-Only] Utilities for <see cref="ConversionCache{TKey, TValue}"/>.</summary>
/// <remarks>This class doesn't use any Editor-Only functionality, but it's unlikely to be useful at runtime.</remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ConversionCache
/// https://kybernetik.com.au/inspector-gadgets/api/InspectorGadgets.Editor/ConversionCache
///
public static class ConversionCache
{
/************************************************************************************************************************/
/// <summary>
/// Creates a <see cref="ConversionCache{TKey, TValue}"/> for calculating the GUI width occupied by text using
/// the specified `style`.
/// </summary>
public static ConversionCache<string, float> CreateWidthCache(GUIStyle style)
=> new(style.CalculateWidth);
/************************************************************************************************************************/
// The "g" format gives a lower case 'e' for exponentials instead of upper case 'E'.
private static readonly ConversionCache<float, string>
FloatToString = new((value) => $"{value:g}");
/// <summary>[Animancer Extension]
/// Calls <see cref="float.ToString(string)"/> using <c>"g"</c> as the format and caches the result.
/// </summary>
public static string ToStringCached(this float value)
=> FloatToString.Convert(value);
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 1b0e970556a97204991d267c5cdb3ca9
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: