first commit

This commit is contained in:
lethanhsonvsp
2025-11-17 15:16:36 +07:00
commit a40d0921eb
17012 changed files with 2652386 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using UnityEngine.Splines;
using UnityEngine.UIElements;
namespace UnityEditor.Splines
{
sealed class BezierTangentPropertyField<T> : DropdownField
where T : ISelectableElement
{
static readonly SplineGUIUtility.EqualityComparer<T> s_Comparer = (a, b) =>
EditorSplineUtility.GetKnot(a).Mode == EditorSplineUtility.GetKnot(b).Mode;
static readonly List<string> k_Modes = new List<string>{L10n.Tr("Mirrored"), L10n.Tr("Continuous"), L10n.Tr("Broken")};
static readonly string k_Tooltip = L10n.Tr(
"Mirrored Tangents:\nIf Knot or InTangent is selected, OutTangent will be mirrored on InTangent. Else, InTangent will be mirrored on OutTangent.\n" +
"Continuous Tangents:\nInTangent and OutTangent are always aligned.\n" +
"Broken Tangents:\nInTangent and OutTangent are dissociated.\n"
);
IReadOnlyList<T> m_Elements = new List<T>(0);
public event Action changed;
internal BezierTangentPropertyField() : base(k_Modes, 0)
{
label = L10n.Tr("Bezier");
name = "ModesDropdown";
tooltip = k_Tooltip;
this.RegisterValueChangedCallback(OnValueChange);
}
bool ShouldShow(IReadOnlyList<T> targets)
{
// Don't show if an element in the selection isn't a bezier mode
for (int i = 0; i < targets.Count; ++i)
{
var knot = EditorSplineUtility.GetKnot(targets[i]);
if (!SplineUtility.AreTangentsModifiable(knot.Mode))
return false;
}
return true;
}
static TangentMode GetTangentModeFromIndex(int index)
{
switch (index)
{
case 0: return TangentMode.Mirrored;
case 1: return TangentMode.Continuous;
case 2: return TangentMode.Broken;
default: return TangentMode.Mirrored;
}
}
static int GetIndexFromTangentMode(TangentMode mode)
{
switch (mode)
{
case TangentMode.Mirrored: return 0;
case TangentMode.Continuous: return 1;
case TangentMode.Broken: return 2;
default: return 0;
}
}
public void Update(IReadOnlyList<T> targets)
{
SetEnabled(ShouldShow(targets));
m_Elements = targets;
SetValueWithoutNotify(EditorSplineUtility.GetKnot(targets[0]).Mode);
showMixedValue = SplineGUIUtility.HasMultipleValues(targets, s_Comparer);
}
public void SetValueWithoutNotify(TangentMode mode)
{
SetValueWithoutNotify(k_Modes[GetIndexFromTangentMode(mode)]);
}
void OnValueChange(ChangeEvent<string> evt)
{
var index = k_Modes.IndexOf(evt.newValue);
if (index < 0)
return;
showMixedValue = false;
var targetMode = GetTangentModeFromIndex(index);
EditorSplineUtility.RecordSelection(SplineInspectorOverlay.SplineChangeUndoMessage);
for (int i = 0; i < m_Elements.Count; ++i)
{
var knot = EditorSplineUtility.GetKnot(m_Elements[i]);
if (m_Elements[i] is SelectableTangent tangent)
knot.SetTangentMode(targetMode, (BezierTangent)tangent.TangentIndex);
else
knot.Mode = targetMode;
}
changed?.Invoke();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 84ba2b41d2954a2d8adc64619daa77e8
timeCreated: 1649959836

View File

@@ -0,0 +1,40 @@
namespace UnityEditor.Splines
{
sealed class CommonElementDrawer : ElementDrawer<ISelectableElement>
{
readonly TangentModePropertyField<ISelectableElement> m_Mode;
readonly BezierTangentPropertyField<ISelectableElement> m_BezierMode;
public CommonElementDrawer()
{
Add(m_Mode = new TangentModePropertyField<ISelectableElement>());
m_Mode.changed += () => { m_BezierMode.Update(targets); };
Add(m_BezierMode = new BezierTangentPropertyField<ISelectableElement>());
m_BezierMode.changed += () => { m_Mode.Update(targets); };
}
public override void Update()
{
base.Update();
m_Mode.Update(targets);
m_BezierMode.Update(targets);
}
public override string GetLabelForTargets()
{
int knotCount = 0;
int tangentCount = 0;
for (int i = 0; i < targets.Count; ++i)
{
if (targets[i] is SelectableKnot)
++knotCount;
else if (targets[i] is SelectableTangent)
++tangentCount;
}
return $"<b>({knotCount}) Knots</b>, <b>({tangentCount}) Tangents</b> selected";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d0dd2407e33307e4386616f5c33463fa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,37 @@
using System.Collections.Generic;
using UnityEngine.Splines;
using UnityEngine.UIElements;
namespace UnityEditor.Splines
{
interface IElementDrawer
{
bool HasKnot(Spline spline, int index);
void PopulateTargets(IReadOnlyList<SplineInfo> splines);
void Update();
string GetLabelForTargets();
}
abstract class ElementDrawer<T> : VisualElement, IElementDrawer where T : ISelectableElement
{
public List<T> targets { get; } = new List<T>();
public T target => targets[0];
public virtual void Update() {}
public virtual string GetLabelForTargets() => string.Empty;
public bool HasKnot(Spline spline, int index)
{
foreach (var t in targets)
if (t.SplineInfo.Spline == spline && t.KnotIndex == index)
return true;
return false;
}
public void PopulateTargets(IReadOnlyList<SplineInfo> splines)
{
SplineSelection.GetElements(splines, targets);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 95245c7aa2899544b8fe0daa4947e0be
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,119 @@
using System;
using UnityEngine.Splines;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace UnityEditor.Splines
{
sealed class ElementInspector : VisualElement, IDisposable
{
public static bool ignoreKnotCallbacks = false;
static readonly string k_NoSelectionMessage = L10n.Tr("No element selected");
readonly Label m_KnotIdentifierLabel;
readonly Label m_ErrorMessage;
readonly SplineActionButtons m_SplineActionButtons;
readonly VisualElement m_SplineDrawerRoot;
IElementDrawer m_ElementDrawer;
readonly CommonElementDrawer m_CommonElementDrawer = new CommonElementDrawer();
readonly BezierKnotDrawer m_BezierKnotDrawer = new BezierKnotDrawer();
readonly TangentDrawer m_TangentDrawer = new TangentDrawer();
static StyleSheet s_CommonStyleSheet;
static StyleSheet s_ThemeStyleSheet;
public ElementInspector()
{
if (s_CommonStyleSheet == null)
s_CommonStyleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Packages/com.unity.splines/Editor/Stylesheets/SplineInspectorCommon.uss");
if (s_ThemeStyleSheet == null)
s_ThemeStyleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>($"Packages/com.unity.splines/Editor/Stylesheets/SplineInspector{(EditorGUIUtility.isProSkin ? "Dark" : "Light")}.uss");
styleSheets.Add(s_CommonStyleSheet);
styleSheets.Add(s_ThemeStyleSheet);
Add(m_KnotIdentifierLabel = new Label());
m_KnotIdentifierLabel.style.height = 24;
m_KnotIdentifierLabel.style.unityTextAlign = TextAnchor.MiddleLeft;
Add(m_ErrorMessage = new Label { name = "ErrorMessage"});
Add(m_SplineDrawerRoot = new VisualElement());
Add(m_SplineActionButtons = new SplineActionButtons());
Spline.Changed += OnKnotModified;
}
public void Dispose()
{
Spline.Changed -= OnKnotModified;
}
void OnKnotModified(Spline spline, int index, SplineModification modification)
{
if (modification == SplineModification.KnotModified && !ignoreKnotCallbacks && m_ElementDrawer != null && m_ElementDrawer.HasKnot(spline, index))
m_ElementDrawer.Update();
}
public void UpdateSelection(IReadOnlyList<SplineInfo> selectedSplines)
{
UpdateDrawerForElements(selectedSplines);
if (SplineSelection.Count < 1 || m_ElementDrawer == null)
{
ShowErrorMessage(k_NoSelectionMessage);
m_KnotIdentifierLabel.style.display = DisplayStyle.None;
m_SplineActionButtons.style.display = DisplayStyle.None;
}
else
{
HideErrorMessage();
m_ElementDrawer.PopulateTargets(selectedSplines);
m_ElementDrawer.Update();
m_KnotIdentifierLabel.text = m_ElementDrawer.GetLabelForTargets();
m_KnotIdentifierLabel.style.display = DisplayStyle.Flex;
m_SplineActionButtons.style.display = DisplayStyle.Flex;
}
m_SplineActionButtons.RefreshSelection(selectedSplines);
}
void UpdateDrawerForElements(IReadOnlyList<SplineInfo> selectedSplines)
{
bool hasKnot = SplineSelection.HasAny<SelectableKnot>(selectedSplines);
bool hasTangent = SplineSelection.HasAny<SelectableTangent>(selectedSplines);
IElementDrawer targetDrawer;
if (hasKnot && hasTangent)
targetDrawer = m_CommonElementDrawer;
else if (hasKnot)
targetDrawer = m_BezierKnotDrawer;
else if (hasTangent)
targetDrawer = m_TangentDrawer;
else
targetDrawer = null;
if (targetDrawer == m_ElementDrawer)
return;
m_SplineDrawerRoot.Clear();
if (targetDrawer != null)
m_SplineDrawerRoot.Add((VisualElement) targetDrawer);
m_ElementDrawer = targetDrawer;
}
void ShowErrorMessage(string error)
{
m_ErrorMessage.style.display = DisplayStyle.Flex;
m_ErrorMessage.text = error;
}
void HideErrorMessage()
{
m_ErrorMessage.style.display = DisplayStyle.None;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b5d8a953f9ef52441bd58f4c1e5853a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,130 @@
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
/// <summary>
/// Creates a property drawer for <see cref="EmbeddedSplineData"/> types.
/// </summary>
/// <seealso cref="EmbeddedSplineDataFieldsAttribute"/>
[CustomPropertyDrawer(typeof(EmbeddedSplineData))]
[CustomPropertyDrawer(typeof(EmbeddedSplineDataFieldsAttribute))]
public class EmbeddedSplineDataPropertyDrawer : PropertyDrawer
{
bool m_AttemptedFindSplineContainer;
static readonly string k_SplineDataKeyContent = "Key";
static Rect ReserveLine(ref Rect rect, int lines = 1)
{
var ret = SplineGUIUtility.ReserveSpace(EditorGUIUtility.singleLineHeight * lines, ref rect);
rect.y += EditorGUIUtility.standardVerticalSpacing * lines;
return ret;
}
static int GetSetBitCount(EmbeddedSplineDataField fields)
{
int c = 0, e = (int) fields;
for(int i = 0; i < 4; ++i)
if ((e & (1 << i)) != 0)
++c;
return c;
}
/// <summary>
/// Gets the height of a SerializedProperty in pixels.
/// </summary>
/// <param name="property">The SerializedProperty to calculate height for.</param>
/// <param name="label">The label of the SerializedProperty.</param>
/// <returns>The height of a SerializedProperty in pixels.</returns>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
var flags = attribute is EmbeddedSplineDataFieldsAttribute attrib
? attrib.Fields
: EmbeddedSplineDataField.All;
if (!property.isExpanded)
return EditorGUIUtility.singleLineHeight;
// reserve one line for foldout
float height = EditorGUIUtility.singleLineHeight;
height += (EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing) * GetSetBitCount(flags);
return height;
}
/// <summary>
/// Creates an interface for a SerializedProperty with an <see cref="EmbeddedSplineData"/> type.
/// </summary>
/// <param name="position">Rectangle on the screen to use for the property GUI.</param>
/// <param name="property">The SerializedProperty to make the custom GUI for.</param>
/// <param name="label">The label of this property.</param>
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var flags = attribute is EmbeddedSplineDataFieldsAttribute filter
? filter.Fields
: EmbeddedSplineDataField.All;
var fields = GetSetBitCount(flags);
var container = property.FindPropertyRelative("m_Container");
var index = property.FindPropertyRelative("m_SplineIndex");
var key = property.FindPropertyRelative("m_Key");
var type = property.FindPropertyRelative("m_Type");
if (fields > 1)
{
property.isExpanded = EditorGUI.Foldout(ReserveLine(ref position),
property.isExpanded,
label?.text ?? property.displayName);
if (!property.isExpanded)
return;
}
label = fields == 1 ? label : null;
// don't create key in property editor
// don't copy key values to empty targets
EditorGUI.BeginChangeCheck();
// only attempt to reconcile the SplineContainer value once per lifetime of drawer. otherwise you get some
// odd behaviour when trying to delete or replace the value in the inspector.
if (!m_AttemptedFindSplineContainer
&& container.objectReferenceValue == null
&& property.serializedObject.targetObject is Component cmp
&& cmp.TryGetComponent<SplineContainer>(out var spcnt))
{
container.objectReferenceValue = spcnt;
GUI.changed = true;
}
m_AttemptedFindSplineContainer = true;
if((flags & EmbeddedSplineDataField.Container) == EmbeddedSplineDataField.Container)
EditorGUI.PropertyField(ReserveLine(ref position), container, label);
if (!(container?.objectReferenceValue is SplineContainer component))
component = null;
if ((flags & EmbeddedSplineDataField.SplineIndex) == EmbeddedSplineDataField.SplineIndex)
SplineGUI.SplineIndexField(ReserveLine(ref position), index, label, component);
if ((flags & EmbeddedSplineDataField.Key) == EmbeddedSplineDataField.Key)
{
string[] keys = component == null || index.intValue < 0 || index.intValue >= component.Splines.Count
? Array.Empty<string>()
: component[index.intValue].GetSplineDataKeys((EmbeddedSplineDataType) type.enumValueIndex).ToArray();
var i = Array.IndexOf(keys, key.stringValue);
EditorGUI.BeginChangeCheck();
i = EditorGUI.Popup(ReserveLine(ref position), label?.text ?? k_SplineDataKeyContent, i, keys);
if (EditorGUI.EndChangeCheck())
key.stringValue = keys[i];
}
if((flags & EmbeddedSplineDataField.Type) == EmbeddedSplineDataField.Type)
EditorGUI.PropertyField(ReserveLine(ref position), type);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 44f1bb87db78405e95edea0e310287b2
timeCreated: 1671225065

View File

@@ -0,0 +1,206 @@
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
[CustomPropertyDrawer(typeof(TangentMode))]
class TangentModePropertyDrawer : PropertyDrawer
{
static readonly GUIContent[] k_TangentModeLabels = new[]
{
new GUIContent("Auto", "The path from this knot to the preceding and following knots is a bezier curve that is automatically calculated from the neighboring knot positions."),
new GUIContent("Linear", "The path from this knot to the preceding and following knots is a straight line."),
new GUIContent("Bezier", "The path from this knot to the preceding and following knots is a bezier curve with manually defined tangents."),
};
static readonly string[] k_BezierModeLabels = new[]
{
"Mirrored",
"Continuous",
"Broken"
};
public static float GetPropertyHeight() => SplineGUIUtility.lineHeight * 2 + 2;
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) => GetPropertyHeight();
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
int mode = property.enumValueIndex;
int type = System.Math.Min(mode, (int) TangentMode.Mirrored);
int bezier = System.Math.Max(mode, (int) TangentMode.Mirrored) - (int)TangentMode.Mirrored;
bool mixed = EditorGUI.showMixedValue;
EditorGUI.BeginChangeCheck();
EditorGUI.showMixedValue = property.hasMultipleDifferentValues;
type = GUI.Toolbar(EditorGUI.IndentedRect(SplineGUIUtility.ReserveSpace(SplineGUIUtility.lineHeight, ref position)),
type,
k_TangentModeLabels);
if (EditorGUI.EndChangeCheck())
{
// Continuous mode should be prefered instead of Mirrored when switching from AutoSmooth to Bezier
// as Centripetal Catmull-Rom's tangents are not always of equal length.
if (property.enumValueIndex == (int)TangentMode.AutoSmooth && type == (int)TangentMode.Mirrored)
type = (int)TangentMode.Continuous;
property.enumValueIndex = type;
}
position.y += 2;
position.height = SplineGUIUtility.lineHeight;
EditorGUI.BeginDisabledGroup(mode < (int) TangentMode.Mirrored);
EditorGUI.BeginChangeCheck();
EditorGUI.showMixedValue = property.hasMultipleDifferentValues;
bezier = EditorGUI.Popup(position, "Bezier", bezier, k_BezierModeLabels);
if (EditorGUI.EndChangeCheck())
property.enumValueIndex = bezier + (int)TangentMode.Mirrored;
EditorGUI.EndDisabledGroup();
EditorGUI.showMixedValue = mixed;
}
}
/// <summary>
/// Specialized UI for drawing a knot property drawer with additional data (ex, TangentMode from Spline.MetaData).
/// Additionally supports inline fields a little longer than the regular inspector wide mode would allow.
/// </summary>
static class KnotPropertyDrawerUI
{
static readonly GUIContent k_Position = EditorGUIUtility.TrTextContent("Position");
static readonly GUIContent k_Rotation = EditorGUIUtility.TrTextContent("Rotation");
static readonly GUIContent k_TangentLengthContent = EditorGUIUtility.TrTextContent("Tangent Length");
static readonly GUIContent k_TangentLengthContentIn = EditorGUIUtility.TrTextContent("Tangent In Length");
static readonly GUIContent k_TangentLengthContentOut = EditorGUIUtility.TrTextContent("Tangent Out Length");
const float k_IndentPad = 13f; // kIndentPerLevel - margin (probably)
const int k_MinWideModeWidth = 230;
const int k_WideModeInputFieldWidth = 212;
static bool CanForceWideMode() => EditorGUIUtility.currentViewWidth > k_MinWideModeWidth;
public static float GetPropertyHeight(SerializedProperty knot, SerializedProperty meta, GUIContent _)
{
// title
float height = SplineGUIUtility.lineHeight;
// position, rotation
height += SplineGUIUtility.lineHeight * (CanForceWideMode() ? 2 : 4);
// 1. { linear, auto, bezier }
// 2. { broken, continuous, mirrored }
height += meta == null ? 0 : TangentModePropertyDrawer.GetPropertyHeight();
// 3. (optional) tangent in
// 4. (optional) tangent out
height += TangentGetPropertyHeight(meta);
return knot.isExpanded ? height : SplineGUIUtility.lineHeight;
}
public static float TangentGetPropertyHeight(SerializedProperty meta)
{
var prop = meta?.FindPropertyRelative("Mode");
var mode = meta == null ? TangentMode.Broken : (TangentMode) prop.enumValueIndex;
switch (mode)
{
case TangentMode.AutoSmooth:
case TangentMode.Linear:
return 0;
case TangentMode.Mirrored:
return SplineGUIUtility.lineHeight;
case TangentMode.Continuous:
return SplineGUIUtility.lineHeight * 2;
case TangentMode.Broken:
default:
return SplineGUIUtility.lineHeight * (CanForceWideMode() ? 2 : 4);
}
}
public static void TangentOnGUI(ref Rect rect,
SerializedProperty tangentIn,
SerializedProperty tangentOut,
TangentMode mode)
{
if (mode == TangentMode.Broken)
{
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpaceForLine(ref rect), tangentIn);
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpaceForLine(ref rect), tangentOut);
return;
}
// tangents are not serialized as vec3, they are a generic type
var tin = tangentIn.FindPropertyRelative("z");
var tout = tangentOut.FindPropertyRelative("z");
if (mode == TangentMode.Mirrored)
{
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpace(SplineGUIUtility.lineHeight, ref rect), tout, k_TangentLengthContent);
tin.floatValue = -tout.floatValue;
}
else if (mode == TangentMode.Continuous)
{
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpace(SplineGUIUtility.lineHeight, ref rect), tin, k_TangentLengthContentIn);
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpace(SplineGUIUtility.lineHeight, ref rect), tout, k_TangentLengthContentOut);
}
}
public static bool OnGUI(Rect rect, SerializedProperty knot, SerializedProperty meta, GUIContent label)
{
bool wideMode = EditorGUIUtility.wideMode;
if (!wideMode && CanForceWideMode())
{
EditorGUIUtility.wideMode = true;
EditorGUIUtility.labelWidth = EditorGUIUtility.currentViewWidth - k_WideModeInputFieldWidth;
}
else
EditorGUIUtility.labelWidth = 0;
var titleRect = SplineGUIUtility.ReserveSpace(SplineGUIUtility.lineHeight, ref rect);
titleRect.width = EditorGUIUtility.labelWidth;
knot.isExpanded = EditorGUI.Foldout(titleRect, knot.isExpanded, label);
var position = knot.FindPropertyRelative("Position");
EditorGUI.BeginChangeCheck();
if (knot.isExpanded)
{
var mode = meta?.FindPropertyRelative("Mode");
bool modeAllowsRotationAndTangent = mode?.enumValueIndex > (int) TangentMode.Linear;
var tangentMode = mode != null ? (TangentMode) mode.enumValueIndex : TangentMode.Broken;
var rotation = knot.FindPropertyRelative("Rotation");
var tangentIn = knot.FindPropertyRelative("TangentIn");
var tangentOut = knot.FindPropertyRelative("TangentOut");
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpaceForLine(ref rect), position, k_Position);
EditorGUI.BeginDisabledGroup(!modeAllowsRotationAndTangent);
SplineGUILayout.QuaternionField(SplineGUIUtility.ReserveSpaceForLine(ref rect), k_Rotation, rotation);
EditorGUI.EndDisabledGroup();
if (meta != null)
{
EditorGUI.PropertyField(rect, mode);
rect.y += TangentModePropertyDrawer.GetPropertyHeight();
}
EditorGUI.BeginDisabledGroup(!modeAllowsRotationAndTangent);
TangentOnGUI(ref rect, tangentIn, tangentOut, tangentMode);
EditorGUI.EndDisabledGroup();
}
// When in wide mode, show the position field inline with the knot title if not expanded
else if (EditorGUIUtility.wideMode)
{
var inlinePositionRect = titleRect;
inlinePositionRect.x += titleRect.width - k_IndentPad * EditorGUI.indentLevel;
inlinePositionRect.width = rect.width - (titleRect.width - k_IndentPad * EditorGUI.indentLevel);
EditorGUI.PropertyField(inlinePositionRect, position, GUIContent.none);
}
EditorGUIUtility.wideMode = wideMode;
EditorGUIUtility.labelWidth = 0;
return EditorGUI.EndChangeCheck();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f3e99075e258cf04898636e5e72ec3e5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,304 @@
using UnityEditorInternal;
using System.Collections.Generic;
using System.Linq;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
class KnotReorderableList : ReorderableList
{
ISplineContainer m_Container;
int m_ContainerIndex;
Spline m_Spline;
SerializedProperty m_KnotElement, m_MetaElement;
SerializedObject serializedObject => serializedProperty.serializedObject;
static Dictionary<int, KnotReorderableList> s_Pool = new();
static KnotReorderableList()
{
Selection.selectionChanged += ClearPool;
SplineSelection.changed += SyncKnotSelection;
}
static int GetPropertyHash(SerializedProperty prop)
{
if(prop.serializedObject.targetObject == null)
return 0;
unchecked
{
int hash = prop.serializedObject.targetObject.GetInstanceID();
hash = hash * 31 * prop.propertyPath.GetHashCode();
return hash;
}
}
static void ClearPool() => s_Pool.Clear();
static void SyncKnotSelection()
{
foreach (var kvp in s_Pool)
kvp.Value.SyncSelection();
// InspectorWindow is private for some reason.
foreach (var win in Resources.FindObjectsOfTypeAll<EditorWindow>())
if (win.GetType().Name.Contains("Inspector"))
win.Repaint();
}
public static KnotReorderableList Get(SerializedProperty splineProperty)
{
int hash = GetPropertyHash(splineProperty);
if (!s_Pool.TryGetValue(hash, out var list))
s_Pool.Add(hash, list = new KnotReorderableList(splineProperty));
list.Init(splineProperty);
return list;
}
public KnotReorderableList(SerializedProperty splineProperty)
: base(splineProperty.serializedObject, splineProperty.FindPropertyRelative("m_Knots"), true, false, true, true)
{
Init(splineProperty);
multiSelect = true;
elementHeightCallback = GetElementHeight;
drawElementCallback = OnDrawElement;
onReorderCallbackWithDetails = OnReorder;
onSelectCallback = OnSelect;
onAddCallback = OnAdd;
onRemoveCallback = OnRemove;
SyncSelection();
}
void Init(SerializedProperty splineProperty)
{
m_KnotElement = splineProperty.FindPropertyRelative("m_Knots");
serializedProperty = m_KnotElement;
m_MetaElement = splineProperty.FindPropertyRelative("m_MetaData");
// only set the ISplineContainer if we are able to determine the correct index into the splines array
if (serializedObject.targetObject is ISplineContainer container)
{
// make sure that this correctly handles the case where ISplineContainer does not serialize an array.
// in these cases we'll assert that there is only one spline. if that isn't the case we can't reasonably
// handle it so just screw it and give up; the UI can accomodate the case where no container is present.
if (SerializedPropertyUtility.TryGetSplineIndex(splineProperty, out m_ContainerIndex))
{
m_Container = container;
if (container.Splines.Count > 0)
{
if (m_Container.Splines.Count < m_ContainerIndex)
m_ContainerIndex = m_Container.Splines.Count - 1;
m_Spline = m_Container.Splines[m_ContainerIndex];
}
}
else if (container.Splines.Count == 1)
{
m_Container = container;
m_Spline = m_Container.Splines[0];
}
}
}
SerializedProperty GetMetaPropertySafe(int i)
{
while(m_MetaElement.arraySize < m_KnotElement.arraySize)
m_MetaElement.InsertArrayElementAtIndex(m_MetaElement.arraySize);
return m_MetaElement.GetArrayElementAtIndex(i);
}
float GetElementHeight(int i) => KnotPropertyDrawerUI.GetPropertyHeight(
m_KnotElement.GetArrayElementAtIndex(i),
GetMetaPropertySafe(i), GUIContent.none);
void OnDrawElement(Rect position, int i, bool isActive, bool isFocused)
{
var knot = m_KnotElement.GetArrayElementAtIndex(i);
var meta = GetMetaPropertySafe(i);
bool guiChanged = false;
// For reasons unknown, a nested reorderable list requires indent to be incremented to draw elements
// with the correct offset. As a hack, we do so when a container is present since we know it will be nested
// within a spline reorderable list.
if (m_Container != null)
{
++EditorGUI.indentLevel;
guiChanged = KnotPropertyDrawerUI.OnGUI(position, knot, meta, new GUIContent($"Knot [{i}]"));
--EditorGUI.indentLevel;
}
else
{
guiChanged = KnotPropertyDrawerUI.OnGUI(position, knot, meta, new GUIContent($"Knot [{i}]"));
}
if (guiChanged && m_Spline != null)
{
serializedObject.ApplyModifiedProperties();
m_Spline.EnforceTangentModeNoNotify(m_Spline.PreviousIndex(i));
m_Spline.EnforceTangentModeNoNotify(i);
m_Spline.EnforceTangentModeNoNotify(m_Spline.NextIndex(i));
m_Spline.SetDirty(SplineModification.KnotModified, i);
// delay repaint because SplineCacheUtility is only clearing it's cache on Spline.afterSplineWasModified
EditorApplication.delayCall += SceneView.RepaintAll;
}
}
static void EnforceTangentModeWithNeighbors(Spline spline, int index)
{
if (spline == null)
return;
int p = spline.PreviousIndex(index), n = spline.NextIndex(index);
spline.EnforceTangentModeNoNotify(index);
if(p != index) spline.EnforceTangentModeNoNotify(p);
if(n != index) spline.EnforceTangentModeNoNotify(n);
}
void OnReorder(ReorderableList reorderableList, int srcIndex, int dstIndex)
{
m_MetaElement.MoveArrayElement(srcIndex, dstIndex);
if (m_Container != null)
{
serializedObject.ApplyModifiedProperties();
m_Container.KnotLinkCollection.KnotIndexChanged(m_ContainerIndex, srcIndex, dstIndex);
serializedObject.Update();
}
EnforceTangentModeWithNeighbors(m_Spline, srcIndex);
EnforceTangentModeWithNeighbors(m_Spline, dstIndex);
m_Spline?.SetDirty(SplineModification.KnotReordered, dstIndex);
}
void OnSelect(ReorderableList reorderableList)
{
if (m_Container != null)
{
SplineSelection.ClearInspectorSelectedSplines();
var selected = selectedIndices.Select(x => new SelectableSplineElement(
new SelectableKnot(new SplineInfo(m_Container, m_ContainerIndex), x))).ToList();
var evt = Event.current;
// At the time of this callback, selectedIndices is already set with the correct selection accounting
// for shift, ctrl and command. If any modifiers where present, we need to _not_ clear the selection
// on other splines but completely replace the selection on this spline.
if (evt.modifiers == EventModifiers.Command
|| evt.modifiers == EventModifiers.Shift
|| evt.modifiers == EventModifiers.Control)
{
selected.AddRange(SplineSelection.selection.Where(x =>
!ReferenceEquals(x.target, m_Container) || x.targetIndex != m_ContainerIndex));
}
SplineSelection.Set(selected);
SceneView.RepaintAll();
}
}
void OnAdd(ReorderableList _)
{
if (m_Container != null)
{
serializedObject.ApplyModifiedProperties();
var selectedIndex = index;
var info = new SplineInfo(m_Container, m_ContainerIndex);
EditorSplineUtility.RecordObject(info, "Add Knot");
if (selectedIndex < count - 1)
{
var knot = EditorSplineUtility.InsertKnot(info, selectedIndex + 1, 0.5f);
SplineSelection.Set(knot);
index = selectedIndex + 1;
Select(selectedIndex + 1);
}
else // last element from the list
{
var knot = new SelectableKnot(info, selectedIndex);
if (knot.IsValid())
{
EditorSplineUtility.AddKnotToTheEnd(
info,
knot.Position + 3f * knot.TangentOut.Direction,
math.rotate(knot.LocalToWorld, math.up()),
knot.TangentOut.Direction);
}
else
{
EditorSplineUtility.AddKnotToTheEnd(
info,
info.Transform.position,
math.up(),
math.forward());
}
index = count;
Select(count);
}
serializedObject.Update();
}
else // if the Spline is not in a ISplineContainer, make default reorderable list
{
defaultBehaviours.DoAddButton(this);
m_MetaElement.InsertArrayElementAtIndex(m_MetaElement.arraySize);
}
}
void OnRemove(ReorderableList _)
{
var toRemove = new List<int>(selectedIndices);
toRemove.Sort();
// Mimic behaviour of a list inspector - if nothing's explicitly selected, remove active or last knot.
if (toRemove.Count == 0 && m_Spline.Count > 0)
toRemove.Add(index);
if (m_Container != null)
{
var info = new SplineInfo(m_Container, m_ContainerIndex);
serializedObject.ApplyModifiedProperties();
Undo.RecordObject(serializedObject.targetObject, "Remove Knot");
for (int i = toRemove.Count - 1; i >= 0; --i)
EditorSplineUtility.RemoveKnot(new SelectableKnot(info, toRemove[i]));
SplineSelection.Clear();
ClearSelection();
serializedObject.Update();
}
else
{
defaultBehaviours.DoRemoveButton(this);
for (int i = toRemove.Count - 1; i >= 0; --i)
m_MetaElement.DeleteArrayElementAtIndex(toRemove[i]);
}
}
public void SyncSelection()
{
ClearSelection();
if (m_Container == null)
return;
foreach (var i in SplineSelection.selection
.Where(x => ReferenceEquals(x.target, m_Container) && x.targetIndex == m_ContainerIndex && x.tangentIndex < 0)
.Select(y => y.knotIndex))
Select(i, true);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 30dcd166c0e84ff5bf290b88ce72bdda
timeCreated: 1669232635

View File

@@ -0,0 +1,24 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace UnityEditor.Splines
{
sealed class ReadOnlyField : BaseField<string>
{
readonly Label m_IndexField;
public ReadOnlyField(string label) : base(label, new Label() { name = "ReadOnlyValue" })
{
style.flexDirection = FlexDirection.Row;
m_IndexField = this.Q<Label>("ReadOnlyValue");
m_IndexField.text = value;
m_IndexField.style.unityTextAlign = TextAnchor.MiddleLeft;
}
public override void SetValueWithoutNotify(string newValue)
{
m_IndexField.text = newValue;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 37aa8727811c9e04785d9c0e4cafc5d3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,137 @@
using System.Collections.Generic;
using UnityEngine.UIElements;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
sealed class SplineActionButtons : VisualElement
{
static readonly string k_LinkButtonLabel = L10n.Tr("Link");
static readonly string k_UnlinkButtonLabel = L10n.Tr("Unlink");
static readonly string k_SplitButtonLabel = L10n.Tr("Split");
static readonly string k_JoinButtonLabel = L10n.Tr("Join");
static readonly string k_ReverseFlowButtonLabel = L10n.Tr("Reverse Spline Flow");
static readonly string k_LinkButtonTooltip = L10n.Tr("Set knots to the same position and link them.");
static readonly string k_UnlinkButtonTooltip = L10n.Tr("Unlink knots.");
static readonly string k_SplitButtonTooltip = L10n.Tr("Divide a knot with two segments into two knots.");
static readonly string k_JoinButtonTooltip = L10n.Tr("Connect the ends of two splines to each other.");
static readonly string k_ReverseFlowButtonTooltip = L10n.Tr("Reverse the direction of a spline.");
static readonly List<SelectableKnot> m_KnotBuffer = new List<SelectableKnot>();
IReadOnlyList<SplineInfo> m_SelectedSplines = new List<SplineInfo>();
readonly Button m_LinkButton;
readonly Button m_UnlinkButton;
readonly Button m_SplitButton;
readonly Button m_JoinButton;
readonly Button m_ReverseFlowButton;
public SplineActionButtons()
{
#if !UNITY_2023_2_OR_NEWER
Add(new Separator());
style.flexDirection = FlexDirection.Column;
var firstRow = new VisualElement();
Add(firstRow);
firstRow.AddToClassList("button-strip");
firstRow.style.flexDirection = FlexDirection.Row;
m_LinkButton = new Button();
m_LinkButton.text = k_LinkButtonLabel;
m_LinkButton.tooltip = k_LinkButtonTooltip;
m_LinkButton.style.flexGrow = new StyleFloat(1);
m_LinkButton.clicked += OnLinkClicked;
m_LinkButton.AddToClassList("button-strip-button");
m_LinkButton.AddToClassList("button-strip-button--left");
firstRow.Add(m_LinkButton);
m_UnlinkButton = new Button();
m_UnlinkButton.text = k_UnlinkButtonLabel;
m_UnlinkButton.tooltip = k_UnlinkButtonTooltip;
m_UnlinkButton.style.flexGrow = new StyleFloat(1);
m_UnlinkButton.clicked += OnUnlinkClicked;
m_UnlinkButton.AddToClassList("button-strip-button");
m_UnlinkButton.AddToClassList("button-strip-button--right");
m_UnlinkButton.AddToClassList("button-strip-button--right-spacer");
firstRow.Add(m_UnlinkButton);
m_SplitButton = new Button();
m_SplitButton.text = k_SplitButtonLabel;
m_SplitButton.tooltip = k_SplitButtonTooltip;
m_SplitButton.style.flexGrow = 1;
m_SplitButton.clicked += OnSplitClicked;
m_SplitButton.AddToClassList("button-strip-button");
m_SplitButton.AddToClassList("button-strip-button--left");
m_SplitButton.AddToClassList("button-strip-button--left-spacer");
firstRow.Add(m_SplitButton);
m_JoinButton = new Button();
m_JoinButton.text = k_JoinButtonLabel;
m_JoinButton.tooltip = k_JoinButtonTooltip;
m_JoinButton.style.flexGrow = 1;
m_JoinButton.clicked += OnJoinClicked;
m_JoinButton.AddToClassList("button-strip-button");
m_JoinButton.AddToClassList("button-strip-button--right");
firstRow.Add(m_JoinButton);
m_ReverseFlowButton = new Button();
m_ReverseFlowButton.text = k_ReverseFlowButtonLabel;
m_ReverseFlowButton.tooltip = k_ReverseFlowButtonTooltip;
m_ReverseFlowButton.style.flexGrow = 1;
m_ReverseFlowButton.clicked += OnReverseFlowClicked;
Add(m_ReverseFlowButton);
#endif
}
void OnLinkClicked()
{
SplineSelection.GetElements(m_SelectedSplines, m_KnotBuffer);
EditorSplineUtility.LinkKnots(m_KnotBuffer);
RefreshSelection(m_SelectedSplines);
}
void OnUnlinkClicked()
{
SplineSelection.GetElements(m_SelectedSplines, m_KnotBuffer);
EditorSplineUtility.UnlinkKnots(m_KnotBuffer);
RefreshSelection(m_SelectedSplines);
}
void OnSplitClicked()
{
EditorSplineUtility.RecordSelection("Split knot");
SplineSelection.Set(EditorSplineUtility.SplitKnot(m_KnotBuffer[0]));
}
void OnJoinClicked()
{
EditorSplineUtility.RecordSelection("Join knot");
SplineSelection.Set(EditorSplineUtility.JoinKnots(m_KnotBuffer[0], m_KnotBuffer[1]));
}
public void RefreshSelection(IReadOnlyList<SplineInfo> selectedSplines)
{
#if !UNITY_2023_2_OR_NEWER
SplineSelection.GetElements(selectedSplines, m_KnotBuffer);
m_LinkButton.SetEnabled(SplineSelectionUtility.CanLinkKnots(m_KnotBuffer));
m_UnlinkButton.SetEnabled(SplineSelectionUtility.CanUnlinkKnots(m_KnotBuffer));
m_SplitButton.SetEnabled(SplineSelectionUtility.CanSplitSelection(m_KnotBuffer));
m_JoinButton.SetEnabled(SplineSelectionUtility.CanJoinSelection(m_KnotBuffer));
m_SelectedSplines = selectedSplines;
#endif
}
void OnReverseFlowClicked()
{
EditorSplineUtility.RecordSelection("Reverse Selected Splines Flow");
EditorSplineUtility.ReverseSplinesFlow(m_SelectedSplines);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 077bfa6de2cd5634cb02f03ec290191d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,19 @@
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
[CustomPropertyDrawer(typeof(SplineDataDictionary<>))]
class SplineDataDictionaryPropertyDrawer : PropertyDrawer
{
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUI.GetPropertyHeight(property.FindPropertyRelative("m_Data")) + EditorGUIUtility.standardVerticalSpacing;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.PropertyField(position, property.FindPropertyRelative("m_Data"), label);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4e526ebdb8c34af5b98d5eb45d4e56a5
timeCreated: 1673297892

View File

@@ -0,0 +1,110 @@
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
[CustomPropertyDrawer(typeof(SplineData<>))]
class SplineDataPropertyDrawer : PropertyDrawer
{
static readonly string k_MultiSplineEditMessage = L10n.Tr("Multi-selection is not supported for SplineData");
static readonly string k_DataUnitTooltip = L10n.Tr("The unit Data Points are using to be associated to the spline. 'Spline Distance' is " +
"using the distance in Unity Units from the spline origin, 'Normalized Distance' is using a normalized value of the spline " +
"length between [0,1] and Knot Index is using Spline Knot indices ");
readonly static GUIContent[] k_PathUnitIndexLabels = new[]
{
new GUIContent(L10n.Tr("Spline Distance")),
new GUIContent(L10n.Tr("Normalized Distance")),
new GUIContent(L10n.Tr("Knot Index"))
};
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
float height = EditorGUIUtility.singleLineHeight;
if (!property.isExpanded || property.serializedObject.isEditingMultipleObjects)
return height;
//Adding space for the object field
height += EditorGUIUtility.standardVerticalSpacing ;
height += EditorGUI.GetPropertyHeight(property.FindPropertyRelative("m_DefaultValue")) + EditorGUIUtility.standardVerticalSpacing ;
height += EditorGUI.GetPropertyHeight(property.FindPropertyRelative("m_IndexUnit")) + EditorGUIUtility.standardVerticalSpacing;
var datapointsProperty = property.FindPropertyRelative("m_DataPoints");
height += EditorGUIUtility.singleLineHeight;
if (datapointsProperty.isExpanded)
{
height += 2 * EditorGUIUtility.singleLineHeight;
var arraySize = datapointsProperty.arraySize;
if (arraySize == 0)
{
height += EditorGUIUtility.singleLineHeight;
}
else
{
for (int dataPointIndex = 0; dataPointIndex < arraySize; dataPointIndex++)
{
height += datapointsProperty.GetArrayElementAtIndex(dataPointIndex).isExpanded
? 3 * EditorGUIUtility.singleLineHeight + 2 * EditorGUIUtility.standardVerticalSpacing
: EditorGUIUtility.singleLineHeight;
}
}
}
return height;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
if (property.serializedObject.isEditingMultipleObjects)
{
EditorGUI.LabelField(position, L10n.Tr(k_MultiSplineEditMessage), EditorStyles.helpBox);
return;
}
property.isExpanded = EditorGUI.Foldout(SplineGUIUtility.ReserveSpace(EditorGUIUtility.singleLineHeight, ref position), property.isExpanded, label);
++EditorGUI.indentLevel;
if (property.isExpanded)
{
SplineGUIUtility.ReserveSpace(EditorGUIUtility.standardVerticalSpacing, ref position);
var valueProperty = property.FindPropertyRelative("m_DefaultValue");
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpace(EditorGUI.GetPropertyHeight(valueProperty), ref position), valueProperty);
SplineGUIUtility.ReserveSpace(EditorGUIUtility.standardVerticalSpacing, ref position);
var indexProperty = property.FindPropertyRelative("m_IndexUnit");
var dataPointsProperty = property.FindPropertyRelative("m_DataPoints");
var pathUnit = (PathIndexUnit)indexProperty.intValue;
EditorGUI.BeginChangeCheck();
var newPathUnit = EditorGUI.Popup(SplineGUIUtility.ReserveSpace(EditorGUI.GetPropertyHeight(indexProperty), ref position),
new GUIContent("Data Index Unit", L10n.Tr(k_DataUnitTooltip)), (int)pathUnit, k_PathUnitIndexLabels);
if (EditorGUI.EndChangeCheck())
{
if (dataPointsProperty.arraySize == 0)
indexProperty.intValue = newPathUnit;
else
{
SplineDataConversionWindow.DoConfirmWindow(
position,
property,
property.serializedObject.targetObject as Component,
newPathUnit,
false);
GUIUtility.ExitGUI();
}
}
SplineGUIUtility.ReserveSpace(EditorGUIUtility.standardVerticalSpacing, ref position);
dataPointsProperty.isExpanded = EditorGUI.Foldout(SplineGUIUtility.ReserveSpace(EditorGUIUtility.singleLineHeight, ref position), dataPointsProperty.isExpanded, new GUIContent("Data Points"));
if (dataPointsProperty.isExpanded)
{
SplineDataReorderableListUtility
.GetDataPointsReorderableList(property, dataPointsProperty, pathUnit).DoList(position);
}
}
--EditorGUI.indentLevel;
EditorGUI.EndProperty();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: db00a1694ad7319449442045b8557a93
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
[InitializeOnLoad]
static class SplineDataReorderableListUtility
{
readonly static string k_DataIndexTooltip = L10n.Tr("The index of the Data Point along the spline and the unit used.");
readonly static string k_DataValueTooltip = L10n.Tr("The value of the Data Point.");
static Dictionary<string, ReorderableList> s_ReorderableLists = new Dictionary<string, ReorderableList>();
static PathIndexUnit s_PathIndexUnit;
readonly static string[] k_DisplayName = new string[] {"Dist","Path %","Knot"};
static SplineDataReorderableListUtility()
{
Selection.selectionChanged += ClearReorderableLists;
}
static void ClearReorderableLists()
{
s_ReorderableLists.Clear();
}
static void SetSplineDataDirty(SerializedProperty splineDataProperty)
{
var splineDataObject = SerializedPropertyUtility.GetSerializedPropertyObject(splineDataProperty);
var dirtyMethod = splineDataObject.GetType().GetMethod("SetDirty", BindingFlags.Instance | BindingFlags.NonPublic);
dirtyMethod?.Invoke(splineDataObject, null);
}
public static ReorderableList GetDataPointsReorderableList(SerializedProperty splineDataProperty, SerializedProperty dataPointProperty, PathIndexUnit unit)
{
var key = dataPointProperty.propertyPath + splineDataProperty.serializedObject.targetObject.GetInstanceID();
s_PathIndexUnit = unit;
if(s_ReorderableLists.TryGetValue(key, out var list))
{
try
{
SerializedProperty.EqualContents(list.serializedProperty, dataPointProperty);
return list;
}
catch(NullReferenceException)
{
s_ReorderableLists.Remove(key);
}
}
list = new ReorderableList(dataPointProperty.serializedObject, dataPointProperty, false, false, true, true);
s_ReorderableLists.Add(key, list);
list.elementHeightCallback = (int index) =>
{
return dataPointProperty.arraySize > 0 && dataPointProperty.GetArrayElementAtIndex(index).isExpanded
? 3 * EditorGUIUtility.singleLineHeight + 2 * EditorGUIUtility.standardVerticalSpacing
: EditorGUIUtility.singleLineHeight;
};
list.onChangedCallback = reorderableList =>
{
SetSplineDataDirty(splineDataProperty);
};
list.drawElementCallback =
(Rect position, int listIndex, bool isActive, bool isFocused) =>
{
var ppte = dataPointProperty.GetArrayElementAtIndex(listIndex);
EditorGUI.indentLevel++;
var expended = EditorGUI.Foldout(SplineGUIUtility.ReserveSpace(EditorGUIUtility.singleLineHeight, ref position), ppte.isExpanded, new GUIContent($"Data Point [{listIndex}]"), true);
if (expended != ppte.isExpanded)
{
ppte.isExpanded = expended;
if (!isActive)
list.index = listIndex;
list.GrabKeyboardFocus();
}
if (ppte.isExpanded)
{
EditorGUI.indentLevel++;
SplineGUIUtility.ReserveSpace(EditorGUIUtility.standardVerticalSpacing, ref position);
EditorGUI.BeginChangeCheck();
var indexProperty = ppte.FindPropertyRelative("m_Index");
EditorGUI.DelayedFloatField(SplineGUIUtility.ReserveSpace(EditorGUIUtility.singleLineHeight, ref position), indexProperty, new GUIContent($"Data Index ({k_DisplayName[(int)s_PathIndexUnit]})", L10n.Tr(k_DataIndexTooltip)));
if(EditorGUI.EndChangeCheck())
{
if (!isActive)
return;
Undo.SetCurrentGroupName("Modify Spline Data point index");
// Apply data point's index change - needs to be applied here so that the sort below is correct.
dataPointProperty.serializedObject.ApplyModifiedProperties();
var newIndex = ppte.FindPropertyRelative("m_Index").floatValue;
var splineDataObject = SerializedPropertyUtility.GetSerializedPropertyObject(splineDataProperty);
var sortMethod = splineDataObject.GetType().GetMethod("ForceSort", BindingFlags.Instance | BindingFlags.NonPublic);
sortMethod?.Invoke(splineDataObject, null);
dataPointProperty.serializedObject.Update();
for (int i = 0; i < dataPointProperty.arraySize; i++)
{
var index = dataPointProperty.GetArrayElementAtIndex(i).FindPropertyRelative("m_Index").floatValue;
if (index == newIndex)
{
list.index = i;
break;
}
}
// Apply the changes resulting from data point sort.
dataPointProperty.serializedObject.ApplyModifiedProperties();
SetSplineDataDirty(splineDataProperty);
}
SplineGUIUtility.ReserveSpace(EditorGUIUtility.standardVerticalSpacing, ref position);
EditorGUI.BeginChangeCheck();
var valueProperty = ppte.FindPropertyRelative("m_Value");
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpace(EditorGUI.GetPropertyHeight(valueProperty), ref position), valueProperty, new GUIContent("Data Value", L10n.Tr(k_DataValueTooltip)));
if (EditorGUI.EndChangeCheck())
{
Undo.SetCurrentGroupName("Modify Spline Data point value");
SetSplineDataDirty(splineDataProperty);
if (!isActive)
list.index = listIndex;
}
EditorGUI.indentLevel--;
}
EditorGUI.indentLevel--;
};
return list;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5bcefd1f1914dff4dae9fe9c4250b8da
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,47 @@
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
/// <summary>
/// Create a property drawer for <see cref="SplineInfo"/> types.
/// </summary>
[CustomPropertyDrawer(typeof(SplineInfo))]
public class SplineInfoPropertyDrawer : PropertyDrawer
{
/// <summary>
/// Returns the height of a SerializedProperty in pixels.
/// </summary>
/// <param name="property">The SerializedProperty to calculate height for.</param>
/// <param name="label">The label of the SerializedProperty.</param>
/// <returns>Returns the height of a SerializedProperty in pixels.</returns>
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorStyles.popup.CalcSize(label).y * 2;
}
/// <summary>
/// Creates an interface for a SerializedProperty with an integer property type.
/// </summary>
/// <param name="position">Rectangle on the screen to use for the property GUI.</param>
/// <param name="property">The SerializedProperty to make the custom GUI for.</param>
/// <param name="label">The label of this property.</param>
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var obj = property.FindPropertyRelative("m_Object");
var con = property.FindPropertyRelative("m_Container");
var ind = property.FindPropertyRelative("m_SplineIndex");
if (con.managedReferenceValue != null && obj.objectReferenceValue == null)
EditorGUI.LabelField(SplineGUIUtility.ReserveSpaceForLine(ref position), "ISplineContainer",
property.managedReferenceFieldTypename);
else
{
EditorGUI.ObjectField(SplineGUIUtility.ReserveSpaceForLine(ref position), obj, typeof(ISplineContainer),
new GUIContent("Spline Container"));
}
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpaceForLine(ref position), ind);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 00a9f482c3791427aad5ae45ad463eda
timeCreated: 1671639448

View File

@@ -0,0 +1,70 @@
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
[CustomPropertyDrawer(typeof(Spline))]
class SplinePropertyDrawer : PropertyDrawer
{
static readonly string[] k_SplineData = new string[]
{
"m_IntData",
"m_FloatData",
"m_Float4Data",
"m_ObjectData"
};
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
if (!property.isExpanded)
return SplineGUIUtility.lineHeight;
float height = SplineGUIUtility.lineHeight * 2;
height += KnotReorderableList.Get(property).GetHeight();
height += EditorGUIUtility.standardVerticalSpacing;
for (int i = 0, c = k_SplineData.Length; i < c; ++i)
height += EditorGUI.GetPropertyHeight(property.FindPropertyRelative(k_SplineData[i]));
return height;
}
// Important note - if this is inspecting a Spline that is not part of an ISplineContainer, callbacks will not
// invoked. That means the Scene View won't reflect changes made in the Inspector without additional code to
// fire the modified callbacks. The easiest way to handle this is to implement ISplineContainer. Alternatively,
// write a custom editor for your class and call Spline.EnforceTangentModeNoNotify() & Spline.SetDirty() after
// any changes.
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
property.isExpanded = EditorGUI.Foldout(
SplineGUIUtility.ReserveSpace(SplineGUIUtility.lineHeight, ref position),
property.isExpanded, label);
if(property.isExpanded)
{
var closedProperty = property.FindPropertyRelative("m_Closed");
EditorGUI.BeginChangeCheck();
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpace(SplineGUIUtility.lineHeight, ref position), closedProperty);
if (EditorGUI.EndChangeCheck() && SerializedPropertyUtility.TryGetSpline(property, out var spline))
{
property.serializedObject.ApplyModifiedProperties();
spline.CheckAutoSmoothExtremityKnots();
spline.SetDirty(SplineModification.ClosedModified);
property.serializedObject.Update();
}
var knots = KnotReorderableList.Get(property);
knots.DoList(position);
position.y += knots.GetHeight() + EditorGUIUtility.standardVerticalSpacing;
for (int i = 0, c = k_SplineData.Length; i < c; ++i)
{
var prop = property.FindPropertyRelative(k_SplineData[i]);
var height = EditorGUI.GetPropertyHeight(prop);
var rect = position;
rect.height = height;
position.y += rect.height;
EditorGUI.PropertyField(rect, prop, true);
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f5dc161b954c4dd6a149b642c8e53bf9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,155 @@
using System.Collections.Generic;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
class SplineReorderableList : ReorderableList
{
SplineContainer m_Container;
static Dictionary<int, SplineReorderableList> s_Pool = new();
static SplineReorderableList()
{
Selection.selectionChanged += ClearPool;
}
static int GetPropertyHash(SerializedProperty prop)
{
if(prop.serializedObject.targetObject == null)
return 0;
unchecked
{
int hash = prop.serializedObject.targetObject.GetInstanceID();
hash = hash * 31 * prop.propertyPath.GetHashCode();
return hash;
}
}
static void ClearPool() => s_Pool.Clear();
public static SplineReorderableList Get(SerializedProperty splineArrayElement)
{
int hash = GetPropertyHash(splineArrayElement);
if (!s_Pool.TryGetValue(hash, out var list))
s_Pool.Add(hash, list = new SplineReorderableList(splineArrayElement.serializedObject, splineArrayElement));
list.Init(splineArrayElement);
return list;
}
void Init(SerializedProperty splineArrayElement)
{
serializedProperty = splineArrayElement;
if (splineArrayElement.serializedObject.targetObject is SplineContainer container)
m_Container = container;
}
public SplineReorderableList(
SerializedObject serializedObject,
SerializedProperty splineArrayElement) : base(serializedObject, splineArrayElement, true, false, true, true)
{
Init(splineArrayElement);
multiSelect = true;
elementHeightCallback += GetElementHeight;
drawElementCallback += DrawElement;
onReorderCallbackWithDetails += OnReorder;
onSelectCallback += OnSelect;
onAddCallback += OnAdd;
onRemoveCallback += OnRemove;
}
float GetElementHeight(int i)
{
return EditorGUI.GetPropertyHeight(serializedProperty.GetArrayElementAtIndex(i));
}
void DrawElement(Rect position, int listIndex, bool isactive, bool isfocused)
{
++EditorGUI.indentLevel;
EditorGUI.BeginChangeCheck();
EditorGUI.PropertyField(position,
serializedProperty.GetArrayElementAtIndex(listIndex),
new GUIContent($"Spline {listIndex}"));
EditorGUI.EndChangeCheck();
--EditorGUI.indentLevel;
}
void OnReorder(ReorderableList reorderableList, int srcIndex, int dstIndex)
{
if (m_Container == null)
return;
Undo.RegisterCompleteObjectUndo(serializedProperty.serializedObject.targetObject, "Reordering Spline in SplineContainer");
m_Container.ReorderSpline(srcIndex, dstIndex);
serializedProperty.serializedObject.ApplyModifiedProperties();
serializedProperty.serializedObject.Update();
}
void OnSelect(ReorderableList _)
{
if (m_Container == null)
return;
SplineSelection.SetInspectorSelectedSplines(m_Container, selectedIndices);
SceneView.RepaintAll();
}
// Used in tests
internal void OnAdd(ReorderableList _)
{
if(m_Container == null)
return;
Undo.RegisterCompleteObjectUndo(serializedProperty.serializedObject.targetObject, "Add new Spline");
int added = 0;
if(selectedIndices.Count > 0)
{
foreach(var i in selectedIndices)
{
var spline = m_Container.AddSpline();
spline.Copy(m_Container.Splines[i]);
m_Container.CopyKnotLinks(i, m_Container.Splines.Count - 1);
added++;
}
}
else
{
var spline = m_Container.AddSpline();
var srcSplineIndex = m_Container.Splines.Count - 2;
if (srcSplineIndex >= 0)
{
spline.Copy(m_Container.Splines[srcSplineIndex]);
m_Container.CopyKnotLinks(srcSplineIndex, m_Container.Splines.Count - 1);
}
added++;
}
int maxCount = m_Container.Splines.Count;
SelectRange(maxCount - added, maxCount - 1);
onSelectCallback(this);
}
// Used in tests
internal void OnRemove(ReorderableList _)
{
if (m_Container == null)
return;
Undo.RegisterCompleteObjectUndo(serializedProperty.serializedObject.targetObject, "Removing Spline from SplineContainer");
for (int i = selectedIndices.Count - 1; i >= 0; i--)
m_Container.RemoveSplineAt(selectedIndices[i]);
PrefabUtility.RecordPrefabInstancePropertyModifications(serializedProperty.serializedObject.targetObject);
ClearSelection();
SplineSelection.ClearInspectorSelectedSplines();
SceneView.RepaintAll();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0dda34381c8748a7a8d49edc59ba0847
timeCreated: 1669140420

View File

@@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using UnityEngine.Splines;
using UnityEngine.UIElements;
namespace UnityEditor.Splines
{
sealed class TangentModePropertyField<T> : VisualElement
where T : ISelectableElement
{
const string k_ButtonStripUssClass = "button-strip";
const string k_ButtonStripButtonUssClass = k_ButtonStripUssClass + "-button";
const string k_ButtonStripButtonLeftUssClass = k_ButtonStripButtonUssClass + "--left";
const string k_ButtonStripButtonMiddleUssClass = k_ButtonStripButtonUssClass + "--middle";
const string k_ButtonStripButtonRightUssClass = k_ButtonStripButtonUssClass + "--right";
const string k_ButtonStripButtonIconUssClass = k_ButtonStripButtonUssClass + "__icon";
const string k_ButtonStripButtonTextUssClass = k_ButtonStripButtonUssClass + "__text";
const string k_ButtonStripButtonCheckedUssClass = k_ButtonStripButtonUssClass + "--checked";
static readonly SplineGUIUtility.EqualityComparer<T> s_Comparer = (a, b) =>
GetModeForProperty(EditorSplineUtility.GetKnot(a).Mode) ==
GetModeForProperty(EditorSplineUtility.GetKnot(b).Mode);
public event Action changed;
const TangentMode k_BezierMode = TangentMode.Mirrored;
readonly Button m_LinearButton;
readonly Button m_AutoSmoothButton;
readonly Button m_BezierButton;
IReadOnlyList<T> m_Elements = new List<T>(0);
TangentMode m_CurrentMode;
internal TangentModePropertyField()
{
AddToClassList(k_ButtonStripUssClass);
Add(m_LinearButton = CreateButton("Linear", L10n.Tr("Linear"), L10n.Tr("Tangents are pointing to the previous/next spline knot.")));
m_LinearButton.AddToClassList(k_ButtonStripButtonLeftUssClass);
m_LinearButton.clickable.clicked += () => OnValueChange(TangentMode.Linear);
Add(m_AutoSmoothButton = CreateButton("AutoSmooth", L10n.Tr("Auto"), L10n.Tr("Tangents are calculated using the previous and next knot positions.")));
m_AutoSmoothButton.AddToClassList(k_ButtonStripButtonMiddleUssClass);
m_AutoSmoothButton.clickable.clicked += () => OnValueChange(TangentMode.AutoSmooth);
Add(m_BezierButton = CreateButton("Bezier", L10n.Tr("Bezier"), L10n.Tr("Tangents are customizable and modifiable.")));
m_BezierButton.AddToClassList(k_ButtonStripButtonRightUssClass);
m_BezierButton.clickable.clicked += () => OnValueChange(TangentMode.Mirrored);
}
static Button CreateButton(string name, string text, string tooltip)
{
var button = new Button{name = name};
button.tooltip = tooltip;
button.AddToClassList(k_ButtonStripButtonUssClass);
var icon = new VisualElement();
icon.AddToClassList(k_ButtonStripButtonIconUssClass);
icon.pickingMode = PickingMode.Ignore;
button.Add(icon);
var label = new TextElement();
label.AddToClassList(k_ButtonStripButtonTextUssClass);
label.pickingMode = PickingMode.Ignore;
label.text = text;
button.Add(label);
return button;
}
public void Update(IReadOnlyList<T> targets)
{
m_Elements = targets;
if (SplineGUIUtility.HasMultipleValues(m_Elements, s_Comparer))
SetToMixedValuesState();
else
SetValueWithoutNotify(EditorSplineUtility.GetKnot(targets[0]).Mode);
}
void SetToMixedValuesState()
{
SetValueWithoutNotify((TangentMode)(-1));
}
void SetValueWithoutNotify(TangentMode mode)
{
m_CurrentMode = GetModeForProperty(mode);
SetButtonChecked(m_LinearButton, m_CurrentMode == TangentMode.Linear);
SetButtonChecked(m_AutoSmoothButton, m_CurrentMode == TangentMode.AutoSmooth);
SetButtonChecked(m_BezierButton, m_CurrentMode == k_BezierMode);
}
void SetButtonChecked(Button button, bool isChecked)
{
button.EnableInClassList(k_ButtonStripButtonCheckedUssClass, isChecked);
}
void OnValueChange(TangentMode mode)
{
if (GetModeForProperty(mode) == m_CurrentMode)
return;
SetValueWithoutNotify(mode);
EditorSplineUtility.RecordSelection(SplineInspectorOverlay.SplineChangeUndoMessage);
for (int i = 0; i < m_Elements.Count; ++i)
{
var knot = EditorSplineUtility.GetKnot(m_Elements[i]);
if (m_Elements[i] is SelectableTangent tangent)
knot.SetTangentMode(mode, (BezierTangent)tangent.TangentIndex);
else
{
// Continuous mode should be prefered instead of Mirrored when switching from AutoSmooth to Bezier
// as Centripetal Catmull-Rom's tangents are not always of equal length.
if (knot.Mode == TangentMode.AutoSmooth && mode == TangentMode.Mirrored)
knot.Mode = TangentMode.Continuous;
else
knot.Mode = mode;
}
}
changed?.Invoke();
}
static TangentMode GetModeForProperty(TangentMode mode)
{
switch (mode)
{
case TangentMode.Continuous:
case TangentMode.Mirrored:
case TangentMode.Broken:
return k_BezierMode;
default:
return mode;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c74ddef7e6e74f529a51b5ad71552fbd
timeCreated: 1637605071

View File

@@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine.Splines;
using UnityEngine.UIElements;
#if !UNITY_2022_1_OR_NEWER
using UnityEditor.UIElements;
#endif
namespace UnityEditor.Splines
{
class TangentPropertyField : VisualElement
{
static readonly List<float> s_LengthBuffer = new List<float>(0);
static readonly SplineGUIUtility.EqualityComparer<float> s_MagnitudeComparer = (a, b) => a.Equals(b);
const string k_TangentFoldoutStyle = "tangent-drawer";
const string k_TangentMagnitudeFloatFieldStyle = "tangent-magnitude-floatfield";
readonly BezierTangent m_Direction;
readonly FloatField m_Magnitude;
public readonly Float3PropertyField<SelectableKnot> vector3field;
IReadOnlyList<SelectableKnot> m_Elements = new List<SelectableKnot>(0);
public event Action changed;
public TangentPropertyField(string text, string vect3name, BezierTangent direction)
{
m_Direction = direction;
//Create Elements
AddToClassList(k_TangentFoldoutStyle);
AddToClassList("unity-base-field");
style.marginBottom = style.marginLeft = style.marginRight = style.marginTop = 0;
var foldout = new Foldout() { value = false };
var foldoutToggle = foldout.Q<Toggle>();
m_Magnitude = new FloatField(L10n.Tr(text), 6);
m_Magnitude.style.flexDirection = FlexDirection.Row;
m_Magnitude.RemoveFromClassList("unity-base-field");
vector3field = new Float3PropertyField<SelectableKnot>("", GetTangentPosition, ApplyPosition) { name = vect3name };
//Build UI Hierarchy
Add(foldout);
foldoutToggle.Add(m_Magnitude);
foldout.Add(vector3field);
foldout.Q<VisualElement>("unity-content").style.marginBottom = 0;
var field = m_Magnitude.Q<VisualElement>("unity-text-input");
field.AddToClassList(k_TangentMagnitudeFloatFieldStyle);
vector3field.changed += () =>
{
Update(m_Elements);
changed?.Invoke();
};
m_Magnitude.RegisterValueChangedCallback((evt) =>
{
var value = evt.newValue;
if (evt.newValue < 0f)
{
m_Magnitude.SetValueWithoutNotify(0f);
value = 0f;
}
EditorSplineUtility.RecordObjects(m_Elements, SplineInspectorOverlay.SplineChangeUndoMessage);
for (var i = 0; i < m_Elements.Count; ++i)
{
var knot = m_Elements[i];
UpdateTangentMagnitude(new SelectableTangent(knot.SplineInfo, knot.KnotIndex, m_Direction), value, m_Direction == BezierTangent.In ? -1f : 1f);
}
m_Magnitude.showMixedValue = false;
Update(m_Elements);
changed?.Invoke();
});
}
public void Update(IReadOnlyList<SelectableKnot> elements)
{
m_Elements = elements;
s_LengthBuffer.Clear();
for (int i = 0; i < elements.Count; ++i)
s_LengthBuffer.Add(math.length((m_Direction == BezierTangent.In ? elements[i].TangentIn : elements[i].TangentOut).LocalPosition));
m_Magnitude.showMixedValue = SplineGUIUtility.HasMultipleValues(s_LengthBuffer, s_MagnitudeComparer);
if (!m_Magnitude.showMixedValue)
m_Magnitude.SetValueWithoutNotify(s_LengthBuffer[0]);
vector3field.Update(elements);
}
float3 GetTangentPosition(SelectableKnot knot)
{
return new SelectableTangent(knot.SplineInfo, knot.KnotIndex, m_Direction).LocalPosition;
}
void ApplyPosition(SelectableKnot knot, float3 position)
{
new SelectableTangent(knot.SplineInfo, knot.KnotIndex, m_Direction) { LocalPosition = position };
}
void UpdateTangentMagnitude(SelectableTangent tangent, float value, float directionSign)
{
var direction = new float3(0, 0, directionSign);
if (math.length(tangent.LocalDirection) > 0)
direction = math.normalize(tangent.LocalDirection);
ElementInspector.ignoreKnotCallbacks = true;
tangent.LocalPosition = value * direction;
ElementInspector.ignoreKnotCallbacks = false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 26228ca4ee1f8eb44b0c159e0fc0ddec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: