chore: initial commit
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// A <see cref="PropertyDrawer"/> for <see cref="IPolymorphic"/> and <see cref="PolymorphicAttribute"/>.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/PolymorphicDrawer
|
||||
[CustomPropertyDrawer(typeof(IPolymorphic), true)]
|
||||
[CustomPropertyDrawer(typeof(PolymorphicAttribute), true)]
|
||||
public sealed class PolymorphicDrawer : PropertyDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private bool _DrawerThrewException;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
var height = 0f;
|
||||
|
||||
if (property.propertyType == SerializedPropertyType.ManagedReference)
|
||||
{
|
||||
GetDetails(property, out var value, out var drawer, out var details);
|
||||
|
||||
if (value == null)
|
||||
return AnimancerGUI.LineHeight;
|
||||
|
||||
if (drawer != null)
|
||||
{
|
||||
if (details.SeparateHeader)
|
||||
{
|
||||
if (!property.isExpanded)
|
||||
return AnimancerGUI.LineHeight;
|
||||
|
||||
height += AnimancerGUI.LineHeight + AnimancerGUI.StandardSpacing;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
height += drawer.GetPropertyHeight(property, label);
|
||||
return height;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_DrawerThrewException = true;
|
||||
Debug.LogException(exception, property.serializedObject.targetObject);
|
||||
// Continue to the regular calculation.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
height += EditorGUI.GetPropertyHeight(property, label, true);
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
if (property.propertyType != SerializedPropertyType.ManagedReference)
|
||||
{
|
||||
EditorGUI.PropertyField(area, property, label, true);
|
||||
return;
|
||||
}
|
||||
|
||||
GetDetails(property, out _, out var drawer, out var details);
|
||||
|
||||
var drawTypeSelectionButton = drawer == null || drawer is not IPolymorphic;
|
||||
|
||||
var button = drawTypeSelectionButton
|
||||
? new TypeSelectionButton(area, property, true)
|
||||
: default;
|
||||
|
||||
if (drawer != null)
|
||||
{
|
||||
if (details.SeparateHeader)
|
||||
{
|
||||
var foldoutArea = area;
|
||||
foldoutArea.width = EditorGUIUtility.labelWidth;
|
||||
foldoutArea.height = AnimancerGUI.LineHeight;
|
||||
|
||||
label = EditorGUI.BeginProperty(foldoutArea, label, property);
|
||||
|
||||
property.isExpanded = EditorGUI.Foldout(foldoutArea, property.isExpanded, label, true);
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
|
||||
area.yMin += AnimancerGUI.LineHeight + AnimancerGUI.StandardSpacing;
|
||||
}
|
||||
|
||||
// If drawing a separate header, don't draw the body if it's collapsed.
|
||||
if (!details.SeparateHeader || property.isExpanded)
|
||||
{
|
||||
try
|
||||
{
|
||||
#pragma warning disable UNT0027 // Do not call PropertyDrawer.OnGUI(). Should only apply to calling base.OnGUI.
|
||||
drawer.OnGUI(area, property, label);
|
||||
#pragma warning restore UNT0027 // Do not call PropertyDrawer.OnGUI().
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_DrawerThrewException = true;
|
||||
Debug.LogException(exception, property.serializedObject.targetObject);
|
||||
// Continue to PropertyField.
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUI.PropertyField(area, property, label, true);
|
||||
}
|
||||
|
||||
if (drawTypeSelectionButton)
|
||||
button.DoGUI();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void GetDetails(
|
||||
SerializedProperty property,
|
||||
out object value,
|
||||
out PropertyDrawer drawer,
|
||||
out PolymorphicDrawerDetails details)
|
||||
{
|
||||
value = property.managedReferenceValue;
|
||||
|
||||
if (_DrawerThrewException || value == null)
|
||||
{
|
||||
drawer = null;
|
||||
details = PolymorphicDrawerDetails.Default;
|
||||
return;
|
||||
}
|
||||
|
||||
if (PropertyDrawers.TryGetDrawer(value.GetType(), fieldInfo, attribute, out drawer) &&
|
||||
drawer is PolymorphicDrawer)
|
||||
drawer = null;
|
||||
|
||||
details = PolymorphicDrawerDetails.Get(value);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3302cfeb4df49034abb712f96989e2ec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,97 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// An assembly attribute for configuring how the <see cref="PolymorphicDrawer"/>
|
||||
/// displays a particular type.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public sealed class PolymorphicDrawerDetails : Attribute
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>A default instance.</summary>
|
||||
public static readonly PolymorphicDrawerDetails
|
||||
Default = new(null);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="System.Type"/> this attribute applies to.</summary>
|
||||
public readonly Type Type;
|
||||
|
||||
/// <summary>Creates a new <see cref="PolymorphicDrawerDetails"/>.</summary>
|
||||
public PolymorphicDrawerDetails(Type type)
|
||||
=> Type = type;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Should the label and <see cref="TypeSelectionButton"/>
|
||||
/// be drawn on a separate line before the field's regular GUI?
|
||||
/// </summary>
|
||||
public bool SeparateHeader { get; set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly Dictionary<Type, PolymorphicDrawerDetails>
|
||||
TypeToDetails = new();
|
||||
|
||||
/// <summary>Gathers all instances of this attribute in all currently loaded assemblies.</summary>
|
||||
static PolymorphicDrawerDetails()
|
||||
{
|
||||
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
for (int iAssembly = 0; iAssembly < assemblies.Length; iAssembly++)
|
||||
{
|
||||
var assembly = assemblies[iAssembly];
|
||||
if (!assembly.IsDefined(typeof(PolymorphicDrawerDetails), false))
|
||||
continue;
|
||||
|
||||
var attributes = assemblies[iAssembly].GetCustomAttributes(typeof(PolymorphicDrawerDetails), false);
|
||||
for (int iAttribute = 0; iAttribute < attributes.Length; iAttribute++)
|
||||
{
|
||||
var attribute = (PolymorphicDrawerDetails)attributes[iAttribute];
|
||||
TypeToDetails.Add(attribute.Type, attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="PolymorphicDrawerDetails"/> associated with the `type` or any of its base types.
|
||||
/// Returns <c>null</c> if none of them have any details.
|
||||
/// </summary>
|
||||
public static PolymorphicDrawerDetails Get(Type type)
|
||||
{
|
||||
if (TypeToDetails.TryGetValue(type, out var details))
|
||||
return details;
|
||||
|
||||
if (type.BaseType != null)
|
||||
details = Get(type.BaseType);
|
||||
else
|
||||
details = Default;
|
||||
|
||||
TypeToDetails.Add(type, details);
|
||||
return details;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="PolymorphicDrawerDetails"/> associated with the `obj` or any of its base types.
|
||||
/// Returns <c>null</c> if none of them have any details.
|
||||
/// </summary>
|
||||
public static PolymorphicDrawerDetails Get(object obj)
|
||||
=> obj == null
|
||||
? Default
|
||||
: Get(obj.GetType());
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ef6fe20da82c4840b0ed0bc86153e54
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,457 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// A button that allows the user to select an object type for a [<see cref="SerializeReference"/>] field.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <strong>Example:</strong>
|
||||
/// <code>
|
||||
/// public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
|
||||
/// {
|
||||
/// using (new TypeSelectionButton(area, property, label, true))
|
||||
/// {
|
||||
/// EditorGUI.PropertyField(area, property, label, true);
|
||||
/// }
|
||||
/// }
|
||||
/// </code></remarks>
|
||||
///
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/TypeSelectionButton
|
||||
///
|
||||
public readonly struct TypeSelectionButton : IDisposable
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The pixel area occupied by the button.</summary>
|
||||
public readonly Rect Area;
|
||||
|
||||
/// <summary>The <see cref="SerializedProperty"/> representing the attributed field.</summary>
|
||||
public readonly SerializedProperty Property;
|
||||
|
||||
/// <summary>The original <see cref="Event.type"/> from when this button was initialized.</summary>
|
||||
public readonly EventType EventType;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="TypeSelectionButton"/>.</summary>
|
||||
public TypeSelectionButton(
|
||||
Rect area,
|
||||
SerializedProperty property,
|
||||
bool hasLabel)
|
||||
{
|
||||
area.height = AnimancerGUI.LineHeight;
|
||||
|
||||
if (hasLabel)
|
||||
area.xMin += EditorGUIUtility.labelWidth + AnimancerGUI.StandardSpacing;
|
||||
|
||||
var currentEvent = Event.current;
|
||||
|
||||
Area = area;
|
||||
Property = property;
|
||||
EventType = currentEvent.type;
|
||||
|
||||
if (Property.propertyType != SerializedPropertyType.ManagedReference)
|
||||
return;
|
||||
|
||||
switch (currentEvent.type)
|
||||
{
|
||||
case EventType.MouseDown:
|
||||
case EventType.MouseUp:
|
||||
if (area.Contains(currentEvent.mousePosition))
|
||||
currentEvent.type = EventType.Ignore;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
void IDisposable.Dispose()
|
||||
=> DoGUI();
|
||||
|
||||
/// <summary>Draws this button's GUI.</summary>
|
||||
/// <remarks>Run this method after drawing the target property so the button draws on top of its label.</remarks>
|
||||
public void DoGUI()
|
||||
{
|
||||
if (Property.propertyType != SerializedPropertyType.ManagedReference)
|
||||
return;
|
||||
|
||||
var currentEvent = Event.current;
|
||||
var eventType = currentEvent.type;
|
||||
var area = Area;
|
||||
|
||||
PrepareSharedReferenceArea(ref area, out var sharedButtonArea, out var value, out var references);
|
||||
|
||||
using (var label = PooledGUIContent.Acquire())
|
||||
{
|
||||
switch (EventType)
|
||||
{
|
||||
case EventType.MouseDown:
|
||||
case EventType.MouseUp:
|
||||
currentEvent.type = EventType;
|
||||
break;
|
||||
|
||||
case EventType.Layout:
|
||||
break;
|
||||
|
||||
// Only Repaint events actually care what the label is.
|
||||
case EventType.Repaint:
|
||||
var valueType = Property.managedReferenceValue?.GetType();
|
||||
if (valueType == null)
|
||||
{
|
||||
label.text = "Null";
|
||||
label.tooltip = "Nothing is assigned";
|
||||
}
|
||||
else
|
||||
{
|
||||
label.text = valueType.GetNameCS(false);
|
||||
label.tooltip = valueType.GetNameCS(true);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (GUI.Button(area, label, EditorStyles.popup))
|
||||
TypeSelectionMenu.Show(Property);
|
||||
}
|
||||
|
||||
DoSharedReferenceGUI(sharedButtonArea, value, references, currentEvent.type);
|
||||
|
||||
if (currentEvent.type == EventType)
|
||||
currentEvent.type = eventType;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Allocates an area for <see cref="DoSharedReferenceGUI"/> if the `value` is shared.</summary>
|
||||
private void PrepareSharedReferenceArea(
|
||||
ref Rect remainingArea,
|
||||
out Rect sharedButtonArea,
|
||||
out object value,
|
||||
out List<SharedReferenceCache.Field> references)
|
||||
{
|
||||
sharedButtonArea = default;
|
||||
value = default;
|
||||
references = default;
|
||||
|
||||
if (!TypeSelectionMenu.VisualiseSharedReferences)
|
||||
return;
|
||||
|
||||
value = Property.managedReferenceValue;
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
var referenceCache = SharedReferenceCache.Get(Property.serializedObject);
|
||||
if (!referenceCache.TryGetInfo(value, out references) ||
|
||||
references.Count <= 1)
|
||||
return;
|
||||
|
||||
sharedButtonArea = AnimancerGUI.StealFromRight(
|
||||
ref remainingArea,
|
||||
remainingArea.height + AnimancerGUI.StandardSpacing * 2,
|
||||
AnimancerGUI.StandardSpacing);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static ConditionalWeakTable<object, object>
|
||||
_VisualiseLinks;
|
||||
private static GUIStyle
|
||||
_SharedReferenceStyle;
|
||||
private static Texture
|
||||
_SharedReferenceIcon;
|
||||
|
||||
/// <summary>Draws a toggle to enable/disable visualisation of the `value`'s shared references.</summary>
|
||||
private void DoSharedReferenceGUI(
|
||||
Rect area,
|
||||
object value,
|
||||
List<SharedReferenceCache.Field> references,
|
||||
EventType eventType)
|
||||
{
|
||||
if (area.width == 0)
|
||||
return;
|
||||
|
||||
var wasVisualising = _VisualiseLinks != null && _VisualiseLinks.TryGetValue(value, out _);
|
||||
var color = eventType == EventType.Repaint
|
||||
? AnimancerGUI.GetHashColor(value.GetHashCode(), 0.5f, 1, 0.7f)
|
||||
: Color.white;
|
||||
|
||||
if (wasVisualising)
|
||||
new LinkLine(area, Property.GetFriendlyPath(), references, color);
|
||||
|
||||
using (var label = PooledGUIContent.Acquire(null, GetTooltip(references, eventType)))
|
||||
{
|
||||
if (_SharedReferenceStyle == null)
|
||||
{
|
||||
_SharedReferenceStyle ??= new(EditorStyles.miniButton)
|
||||
{
|
||||
padding = new RectOffset(0, 0, -2, 0),
|
||||
overflow = new RectOffset(),
|
||||
};
|
||||
|
||||
_SharedReferenceIcon = AnimancerIcons.Load(EditorGUIUtility.isProSkin
|
||||
? "d_Linked@2x"
|
||||
: "Linked@2x");
|
||||
}
|
||||
|
||||
label.image = _SharedReferenceIcon;
|
||||
|
||||
var oldColor = GUI.color;
|
||||
GUI.color = color;
|
||||
|
||||
var isVisualising = GUI.Toggle(area, wasVisualising, label, _SharedReferenceStyle);
|
||||
|
||||
GUI.color = oldColor;
|
||||
|
||||
if (isVisualising != wasVisualising)
|
||||
{
|
||||
if (isVisualising)
|
||||
{
|
||||
_VisualiseLinks ??= new();
|
||||
_VisualiseLinks.Add(value, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
_VisualiseLinks.Remove(value);
|
||||
}
|
||||
}
|
||||
|
||||
label.image = null;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Builds a tooltip describing the `references`.</summary>
|
||||
private static string GetTooltip(
|
||||
List<SharedReferenceCache.Field> references,
|
||||
EventType eventType)
|
||||
{
|
||||
if (eventType != EventType.Repaint)
|
||||
return null;
|
||||
|
||||
var text = StringBuilderPool.Instance.Acquire();
|
||||
|
||||
text.Append("This reference is shared by:");
|
||||
|
||||
for (int i = 0; i < references.Count; i++)
|
||||
text.Append("\n• ").Append(ObjectNames.NicifyVariableName(references[i].path));
|
||||
|
||||
text.Append("\nClick to visualise");
|
||||
|
||||
return text.ReleaseToString();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Link Lines
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly List<LinkLine>
|
||||
LinkLines = new();
|
||||
|
||||
private static int _DelayLinkLines;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Any shared reference link lines which would be drawn after this call are instead
|
||||
/// delayed until the corresponding <see cref="EndDelayingLinkLines"/> call.
|
||||
/// </summary>
|
||||
public static void BeginDelayingLinkLines()
|
||||
{
|
||||
_DelayLinkLines++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends a block started by <see cref="BeginDelayingLinkLines"/>.
|
||||
/// When all such blocks are cancelled, this method draws all delayed links between shared reference fields.
|
||||
/// </summary>
|
||||
public static void EndDelayingLinkLines()
|
||||
{
|
||||
_DelayLinkLines--;
|
||||
|
||||
if (_DelayLinkLines <= 0)
|
||||
{
|
||||
for (int i = LinkLines.Count - 1; i >= 0; i--)
|
||||
LinkLines[i].Draw();
|
||||
|
||||
LinkLines.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The details needed to draw a line between fields which share the same reference.</summary>
|
||||
private readonly struct LinkLine
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The area of the button which toggles visibility of link lines.</summary>
|
||||
public readonly Rect SharedButtonArea;
|
||||
|
||||
/// <summary>The property path of the target field.</summary>
|
||||
public readonly string Path;
|
||||
|
||||
/// <summary>The shared reference cache for the target field.</summary>
|
||||
public readonly List<SharedReferenceCache.Field> References;
|
||||
|
||||
/// <summary>The color of the link line.</summary>
|
||||
public readonly Color Color;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="LinkLine"/>.</summary>
|
||||
public LinkLine(
|
||||
Rect sharedButtonArea,
|
||||
string path,
|
||||
List<SharedReferenceCache.Field> references,
|
||||
Color color)
|
||||
{
|
||||
sharedButtonArea.position += AnimancerGUI.GuiOffset;
|
||||
|
||||
SharedButtonArea = sharedButtonArea;
|
||||
Path = path;
|
||||
References = references;
|
||||
Color = color;
|
||||
|
||||
if (_DelayLinkLines <= 0)
|
||||
Draw();
|
||||
else
|
||||
LinkLines.Add(this);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a line between the current field and the the previous field referencing the same value.</summary>
|
||||
public void Draw()
|
||||
{
|
||||
var currentEvent = Event.current;
|
||||
if (currentEvent.type != EventType.Repaint)
|
||||
return;
|
||||
|
||||
var index = SetArea(References, Path, SharedButtonArea);
|
||||
|
||||
Handles.DrawLine(default, default);// Necessary for DrawCurve to work.
|
||||
|
||||
AnimancerGUI.BeginTriangles(Color);
|
||||
|
||||
var position = GetLeftCenter(SharedButtonArea);
|
||||
|
||||
for (int i = index - 1; i >= 0; i--)
|
||||
{
|
||||
var otherArea = References[i].area;
|
||||
if (otherArea == default)
|
||||
continue;
|
||||
|
||||
var otherPosition = GetLeftCenter(otherArea);
|
||||
DrawCurve(position, otherPosition);
|
||||
break;
|
||||
}
|
||||
|
||||
AnimancerGUI.EndTriangles();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the <see cref="SharedReferenceCache.Field.area"/> of the current field.</summary>
|
||||
private static int SetArea(
|
||||
List<SharedReferenceCache.Field> references,
|
||||
string path,
|
||||
Rect area)
|
||||
{
|
||||
for (int i = 0; i < references.Count; i++)
|
||||
{
|
||||
var reference = references[i];
|
||||
if (reference.path != path)
|
||||
continue;
|
||||
|
||||
reference.area = area;
|
||||
references[i] = reference;
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the center point of the left edge of the `rect`.</summary>
|
||||
private static Vector2 GetLeftCenter(Rect rect)
|
||||
=> new(rect.x, rect.y + rect.height * 0.5f);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a line between `a` and `b` curved towards x = 0.</summary>
|
||||
private static void DrawCurve(
|
||||
Vector2 a,
|
||||
Vector2 b)
|
||||
{
|
||||
const int Segments = 16;
|
||||
const float Increment = 1f / (Segments - 1);
|
||||
|
||||
var width = CalculateCurveWidth(Math.Abs(a.y - b.y));
|
||||
|
||||
var previous = a;
|
||||
|
||||
for (int i = 0; i < Segments; i++)
|
||||
{
|
||||
var t = i * Increment;
|
||||
var next = Vector2.LerpUnclamped(a, b, t);
|
||||
|
||||
var curve = 0.5f - t;
|
||||
curve *= 2;
|
||||
curve *= curve;
|
||||
curve = 1 - curve;
|
||||
next.x *= 1 - curve * width;
|
||||
|
||||
AnimancerGUI.DrawLineBatched(previous, next, 2);
|
||||
|
||||
previous = next;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the desired width for a curve with the given `height`
|
||||
/// as a portion of the total available width.
|
||||
/// </summary>
|
||||
private static float CalculateCurveWidth(float height)
|
||||
{
|
||||
const float
|
||||
MinWidth = 0.05f,
|
||||
MaxWidth = 0.8f;
|
||||
|
||||
var maxHeight = AnimancerGUI.LineHeight * 100;
|
||||
|
||||
if (height > maxHeight)
|
||||
return MaxWidth;
|
||||
|
||||
var t = height / maxHeight;
|
||||
t = 1 - t;
|
||||
t *= t;
|
||||
|
||||
return Mathf.Lerp(MaxWidth, MinWidth, t);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 311697a696013e64fbab8f44f876867e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,352 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] A context menu for selecting a <see cref="Type"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/TypeSelectionMenu
|
||||
public static class TypeSelectionMenu
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private const string
|
||||
PrefKeyPrefix = nameof(TypeSelectionMenu) + ".",
|
||||
PrefMenuPrefix = "Display Options/";
|
||||
|
||||
/// <summary>Should shared references be shown in the GUI?</summary>
|
||||
public static readonly BoolPref
|
||||
VisualiseSharedReferences = new(
|
||||
PrefKeyPrefix + nameof(VisualiseSharedReferences),
|
||||
PrefMenuPrefix + "Visualise Shared References",
|
||||
true);
|
||||
|
||||
/// <summary>Should full type names be displayed?</summary>
|
||||
public static readonly BoolPref
|
||||
UseFullNames = new(
|
||||
PrefKeyPrefix + nameof(UseFullNames),
|
||||
PrefMenuPrefix + "Show Full Names");
|
||||
|
||||
/// <summary>Should options be grouped in sub menus based on their inheritance hierarchy?</summary>
|
||||
public static readonly BoolPref
|
||||
UseTypeHierarchy = new(
|
||||
PrefKeyPrefix + nameof(UseTypeHierarchy),
|
||||
PrefMenuPrefix + "Show Type Hierarchy");
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Shows a menu to select which type of object to assign to the `property`.</summary>
|
||||
public static void Show(SerializedProperty property)
|
||||
{
|
||||
var value = property.managedReferenceValue;
|
||||
var accessor = property.GetAccessor();
|
||||
var fieldType = accessor.GetFieldElementType(property);
|
||||
var selectedType = value?.GetType();
|
||||
|
||||
var menu = new GenericMenu();
|
||||
|
||||
AddPrefs(menu);
|
||||
AddDocumentation(menu, fieldType);
|
||||
|
||||
menu.AddSeparator("");
|
||||
menu.AddDisabledItem(new(ObjectNames.NicifyVariableName(property.GetFriendlyPath())));
|
||||
menu.AddSeparator("");
|
||||
|
||||
AddTypeSelector(menu, property, fieldType, selectedType, null);
|
||||
|
||||
AddSharedReferences(menu, property, fieldType, value);
|
||||
|
||||
var inheritors = GetDerivedTypes(fieldType);
|
||||
for (int i = 0; i < inheritors.Count; i++)
|
||||
AddTypeSelector(menu, property, fieldType, selectedType, inheritors[i]);
|
||||
|
||||
menu.ShowAsContext();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds items for toggling the display options.</summary>
|
||||
private static void AddPrefs(GenericMenu menu)
|
||||
{
|
||||
VisualiseSharedReferences.AddToggleFunction(menu);
|
||||
UseFullNames.AddToggleFunction(menu);
|
||||
UseTypeHierarchy.AddToggleFunction(menu);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds an itme for opening the documentation if a <see cref="HelpURLAttribute"/> is present.</summary>
|
||||
private static void AddDocumentation(
|
||||
GenericMenu menu,
|
||||
Type fieldType)
|
||||
{
|
||||
var help = fieldType.GetCustomAttribute<HelpURLAttribute>();
|
||||
if (help == null ||
|
||||
string.IsNullOrWhiteSpace(help.URL))
|
||||
return;
|
||||
|
||||
var label = $"Documentation: {help.URL.Replace('/', '\\')}";
|
||||
menu.AddItem(new(label), false, () => Application.OpenURL(help.URL));
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds items for selecting shared references.</summary>
|
||||
private static void AddSharedReferences(
|
||||
GenericMenu menu,
|
||||
SerializedProperty property,
|
||||
Type fieldType,
|
||||
object currentValue)
|
||||
{
|
||||
foreach (var item in GetObjectsAndPaths(property.serializedObject, fieldType))
|
||||
{
|
||||
var label = $"Shared Reference/{ObjectNames.NicifyVariableName(item.Value)}";
|
||||
var state = item.Key == currentValue ? MenuFunctionState.Selected : MenuFunctionState.Normal;
|
||||
menu.AddPropertyModifierFunction(property, label, state, targetProperty =>
|
||||
{
|
||||
targetProperty.managedReferenceValue = item.Key;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gathers all potential references that could be shared.</summary>
|
||||
private static List<KeyValuePair<object, string>> GetObjectsAndPaths(
|
||||
SerializedObject serializedObject,
|
||||
Type fieldType)
|
||||
{
|
||||
var objectsAndPaths = new List<KeyValuePair<object, string>>();
|
||||
|
||||
var referenceCache = SharedReferenceCache.Get(serializedObject);
|
||||
referenceCache.GatherReferences();
|
||||
|
||||
foreach (var item in referenceCache)
|
||||
{
|
||||
if (!fieldType.IsAssignableFrom(item.Key.GetType()))
|
||||
continue;
|
||||
|
||||
foreach (var info in item.Value)
|
||||
{
|
||||
objectsAndPaths.Add(new(item.Key, info.path));
|
||||
}
|
||||
}
|
||||
|
||||
objectsAndPaths.Sort(
|
||||
(a, b) => Comparer<string>.Default.Compare(a.Value, b.Value));
|
||||
|
||||
return objectsAndPaths;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds a menu function to assign a new instance of the `newType` to the `property`.</summary>
|
||||
private static void AddTypeSelector(
|
||||
GenericMenu menu,
|
||||
SerializedProperty property,
|
||||
Type fieldType,
|
||||
Type selectedType,
|
||||
Type newType)
|
||||
{
|
||||
var label = GetSelectorLabel(fieldType, newType);
|
||||
var state = selectedType == newType ? MenuFunctionState.Selected : MenuFunctionState.Normal;
|
||||
menu.AddPropertyModifierFunction(property, label, state, targetProperty =>
|
||||
{
|
||||
var oldValue = property.GetValue();
|
||||
var newValue = AnimancerReflection.CreateDefaultInstance(newType);
|
||||
|
||||
CopyCommonFields(oldValue, newValue);
|
||||
|
||||
if (newValue is IPolymorphicReset reset)
|
||||
reset.Reset(oldValue);
|
||||
|
||||
targetProperty.managedReferenceValue = newValue;
|
||||
targetProperty.isExpanded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static string GetSelectorLabel(Type fieldType, Type newType)
|
||||
{
|
||||
if (newType == null)
|
||||
return "Null";
|
||||
|
||||
if (!UseTypeHierarchy)
|
||||
return newType.GetNameCS(UseFullNames);
|
||||
|
||||
var label = StringBuilderPool.Instance.Acquire();
|
||||
|
||||
if (fieldType.IsInterface)// Interface.
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (label.Length > 0)
|
||||
label.Insert(0, '/');
|
||||
|
||||
var displayType = newType.IsGenericType ?
|
||||
newType.GetGenericTypeDefinition() :
|
||||
newType;
|
||||
label.Insert(0, displayType.GetNameCS(UseFullNames));
|
||||
|
||||
newType = newType.BaseType;
|
||||
|
||||
if (newType == null ||
|
||||
!fieldType.IsAssignableFrom(newType))
|
||||
break;
|
||||
}
|
||||
}
|
||||
else// Base Class.
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (label.Length > 0)
|
||||
label.Insert(0, '/');
|
||||
|
||||
label.Insert(0, newType.GetNameCS(UseFullNames));
|
||||
|
||||
newType = newType.BaseType;
|
||||
|
||||
if (newType == null)
|
||||
break;
|
||||
|
||||
if (fieldType.IsAbstract)
|
||||
{
|
||||
if (newType == fieldType)
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (newType == fieldType.BaseType)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return label.ReleaseToString();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly List<Type>
|
||||
AllTypes = new(1024);
|
||||
private static readonly Dictionary<Type, List<Type>>
|
||||
TypeToDerived = new();
|
||||
|
||||
/// <summary>Returns a list of all types that inherit from the `baseType`.</summary>
|
||||
public static List<Type> GetDerivedTypes(Type baseType)
|
||||
{
|
||||
if (!TypeToDerived.TryGetValue(baseType, out var derivedTypes))
|
||||
{
|
||||
if (AllTypes.Count == 0)
|
||||
{
|
||||
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
for (int iAssembly = 0; iAssembly < assemblies.Length; iAssembly++)
|
||||
{
|
||||
var assembly = assemblies[iAssembly];
|
||||
if (assembly.IsDynamic)
|
||||
continue;
|
||||
|
||||
var types = assembly.GetExportedTypes();
|
||||
for (int iType = 0; iType < types.Length; iType++)
|
||||
{
|
||||
var type = types[iType];
|
||||
if (IsViableType(type))
|
||||
AllTypes.Add(type);
|
||||
}
|
||||
}
|
||||
|
||||
AllTypes.Sort((a, b) => a.FullName.CompareTo(b.FullName));
|
||||
}
|
||||
|
||||
derivedTypes = new();
|
||||
for (int i = 0; i < AllTypes.Count; i++)
|
||||
{
|
||||
var type = AllTypes[i];
|
||||
if (baseType.IsAssignableFrom(type))
|
||||
derivedTypes.Add(type);
|
||||
}
|
||||
TypeToDerived.Add(baseType, derivedTypes);
|
||||
}
|
||||
|
||||
return derivedTypes;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Is the `type` supported by <see cref="SerializeReference"/> fields?</summary>
|
||||
public static bool IsViableType(Type type)
|
||||
=> !type.IsAbstract
|
||||
&& !type.IsEnum
|
||||
&& !type.IsGenericTypeDefinition
|
||||
&& !type.IsInterface
|
||||
&& !type.IsPrimitive
|
||||
&& !type.IsSpecialName
|
||||
&& type.Name[0] != '<'
|
||||
&& type.IsDefined(typeof(SerializableAttribute), false)
|
||||
&& !type.IsDefined(typeof(ObsoleteAttribute), true)
|
||||
&& !typeof(Object).IsAssignableFrom(type)
|
||||
&& type.GetConstructor(AnimancerReflection.InstanceBindings, null, Type.EmptyTypes, null) != null;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Copies the values of all fields in `from` into corresponding fields in `to` as long as they have the same
|
||||
/// name and compatible types.
|
||||
/// </summary>
|
||||
public static void CopyCommonFields(object from, object to)
|
||||
{
|
||||
if (from == null ||
|
||||
to == null)
|
||||
return;
|
||||
|
||||
var nameToFromField = new Dictionary<string, FieldInfo>();
|
||||
var fromType = from.GetType();
|
||||
do
|
||||
{
|
||||
var fromFields = fromType.GetFields(AnimancerReflection.InstanceBindings | BindingFlags.DeclaredOnly);
|
||||
|
||||
for (int i = 0; i < fromFields.Length; i++)
|
||||
{
|
||||
var field = fromFields[i];
|
||||
nameToFromField[field.Name] = field;
|
||||
}
|
||||
|
||||
fromType = fromType.BaseType;
|
||||
}
|
||||
while (fromType != null);
|
||||
|
||||
var toType = to.GetType();
|
||||
do
|
||||
{
|
||||
var toFields = toType.GetFields(AnimancerReflection.InstanceBindings | BindingFlags.DeclaredOnly);
|
||||
|
||||
for (int i = 0; i < toFields.Length; i++)
|
||||
{
|
||||
var toField = toFields[i];
|
||||
if (nameToFromField.TryGetValue(toField.Name, out var fromField))
|
||||
{
|
||||
var fromValue = fromField.GetValue(from);
|
||||
if (fromValue == null || toField.FieldType.IsAssignableFrom(fromValue.GetType()))
|
||||
toField.SetValue(to, fromValue);
|
||||
}
|
||||
}
|
||||
|
||||
toType = toType.BaseType;
|
||||
}
|
||||
while (toType != null);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32cf6628710d067439dd6c78226a58a2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user