first commit
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84ba2b41d2954a2d8adc64619daa77e8
|
||||
timeCreated: 1649959836
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d0dd2407e33307e4386616f5c33463fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95245c7aa2899544b8fe0daa4947e0be
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5d8a953f9ef52441bd58f4c1e5853a0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44f1bb87db78405e95edea0e310287b2
|
||||
timeCreated: 1671225065
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f3e99075e258cf04898636e5e72ec3e5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 30dcd166c0e84ff5bf290b88ce72bdda
|
||||
timeCreated: 1669232635
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 37aa8727811c9e04785d9c0e4cafc5d3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 077bfa6de2cd5634cb02f03ec290191d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e526ebdb8c34af5b98d5eb45d4e56a5
|
||||
timeCreated: 1673297892
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db00a1694ad7319449442045b8557a93
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5bcefd1f1914dff4dae9fe9c4250b8da
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 00a9f482c3791427aad5ae45ad463eda
|
||||
timeCreated: 1671639448
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5dc161b954c4dd6a149b642c8e53bf9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0dda34381c8748a7a8d49edc59ba0847
|
||||
timeCreated: 1669140420
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c74ddef7e6e74f529a51b5ad71552fbd
|
||||
timeCreated: 1637605071
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 26228ca4ee1f8eb44b0c159e0fc0ddec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user