2025-05-09 15:40:34 +08:00

189 lines
7.3 KiB
C#

// Animancer // Copyright 2018-2024 Kybernetik //
#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only]
/// A system which gathers information about <see cref="SerializeReference"/> fields to detect when multiple fields
/// are referencing the same object.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/SharedReferenceCache
public class SharedReferenceCache :
IEnumerable<KeyValuePair<object, List<SharedReferenceCache.Field>>>
{
/************************************************************************************************************************/
#region Static Caching
/************************************************************************************************************************/
private static readonly Dictionary<SerializedObject, SharedReferenceCache>
SerializedObjectToCache = new();
/// <summary>Returns a cached <see cref="SharedReferenceCache"/> for the `serializedObject`.</summary>
public static SharedReferenceCache Get(SerializedObject serializedObject)
{
CheckFlush(serializedObject);
if (!SerializedObjectToCache.TryGetValue(serializedObject, out var cache))
SerializedObjectToCache.Add(serializedObject, cache = new(serializedObject));
return cache;
}
/************************************************************************************************************************/
private static readonly HashSet<SerializedObject>
NotRecentlyUsed = new();
private const double
FlushInterval = 5;
private static double
_LastFlushTime;
/// <summary>Discards any caches not used during the last <see cref="FlushInterval"/> when it elapses.</summary>
private static void CheckFlush(SerializedObject serializedObject)
{
var currentTime = EditorApplication.timeSinceStartup;
if (currentTime >= _LastFlushTime + FlushInterval)
{
_LastFlushTime = currentTime;
foreach (var unused in NotRecentlyUsed)
SerializedObjectToCache.Remove(unused);
NotRecentlyUsed.Clear();
NotRecentlyUsed.UnionWith(SerializedObjectToCache.Keys);
}
NotRecentlyUsed.Remove(serializedObject);
}
/************************************************************************************************************************/
/// <summary>The number of editor updates that have occurred since startup.</summary>
public static ulong FrameCount { get; private set; }
static SharedReferenceCache()
{
EditorApplication.update += () => FrameCount++;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
/// <summary>Information about a field.</summary>
public struct Field
{
/************************************************************************************************************************/
/// <summary>The <see cref="Serialization.GetFriendlyPath"/> of the field.</summary>
public string path;
/// <summary>The area where the field was last drawn.</summary>
public Rect area;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="Field"/>.</summary>
public Field(string path)
{
this.path = path;
area = default;
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
private readonly SerializedObject
SerializedObject;
private readonly Dictionary<object, List<Field>>
ObjectToReferences = new();
private ulong
_LastGatherFrameCount;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="SharedReferenceCache"/>.</summary>
public SharedReferenceCache(SerializedObject serializedObject)
{
SerializedObject = serializedObject;
}
/************************************************************************************************************************/
/// <summary>Should <see cref="GatherReferences"/> be called?</summary>
public bool ShouldGather
=> _LastGatherFrameCount != FrameCount;
/// <summary>Updates the cached reference info.</summary>
public void GatherReferences()
{
_LastGatherFrameCount = FrameCount;
ObjectToReferences.Clear();
var property = SerializedObject.GetIterator();
while (property.Next(true))
{
if (property.propertyType != SerializedPropertyType.ManagedReference)
continue;
var reference = property.managedReferenceValue;
if (reference == null)
continue;
if (!ObjectToReferences.TryGetValue(reference, out var paths))
ObjectToReferences.Add(reference, paths = new());
paths.Add(new(property.GetFriendlyPath()));
}
}
/************************************************************************************************************************/
/// <summary>Tries to get the info about all fields containing the `reference`.</summary>
public bool TryGetInfo(object reference, out List<Field> references)
{
if (ShouldGather)
GatherReferences();
return ObjectToReferences.TryGetValue(reference, out references);
}
/************************************************************************************************************************/
/// <summary>Returns an enumerator for all references and their info.</summary>
public Dictionary<object, List<Field>>.Enumerator GetEnumerator()
{
if (ShouldGather)
GatherReferences();
return ObjectToReferences.GetEnumerator();
}
/// <summary>Returns an enumerator for all references and their info.</summary>
IEnumerator<KeyValuePair<object, List<Field>>> IEnumerable<KeyValuePair<object, List<Field>>>.GetEnumerator()
=> GetEnumerator();
/// <summary>Returns an enumerator for all references and their info.</summary>
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/************************************************************************************************************************/
}
}
#endif