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,27 @@
using UnityEditor.EditorTools;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
static class EditorSplineGizmos
{
public static bool showSelectedGizmo = false;
[DrawGizmo(GizmoType.Active | GizmoType.NonSelected | GizmoType.Selected | GizmoType.Pickable)]
// ReSharper disable once Unity.ParameterNotDerivedFromComponent
static void DrawUnselectedSplineGizmos(ISplineContainer provider, GizmoType gizmoType)
{
//Skip if tool engaged is a spline tool
if (typeof(SplineTool).IsAssignableFrom(ToolManager.activeToolType) && !showSelectedGizmo && (gizmoType & GizmoType.Selected) > 0)
return;
var prev = Gizmos.color;
Gizmos.color = (gizmoType & (GizmoType.Selected | GizmoType.Active)) > 0
? Handles.selectedColor
: SplineGizmoUtility.s_GizmosLineColor.value;
SplineGizmoUtility.DrawGizmos(provider);
Gizmos.color = prev;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a0357b2fb04a410d8dc62740f4fe4cbb
timeCreated: 1618421082

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: eaa3cd6309cba3244b2a227f3e8e7eb3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

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:

View File

@@ -0,0 +1,159 @@
using System;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
class SplineDataConversionWindow : EditorWindow
{
static readonly string k_SplineDataConversionMessage = L10n.Tr("Select a reference spline to convert your " +
"SplineData with no data loss. Otherwise data " +
"won't be converted.");
static readonly Regex k_EmbeddedSplineDataContainerIndex =
new Regex(@"((?<=m_Splines\.Array\.data\[)([0-9]+))(?=\])", RegexOptions.Compiled);
SerializedProperty m_SplineDataProperty;
SplineContainer m_TargetContainer;
int m_TargetSpline;
PathIndexUnit m_NewIndexUnit;
string[] m_SplineSelectionContent;
int[] m_SplineSelectionValues;
public static void DoConfirmWindow(Rect rect,
SerializedProperty splineDataProperty,
Component targetComponent,
int newValue,
bool forceShowDialog)
{
var window = Resources.FindObjectsOfTypeAll<SplineDataConversionWindow>().FirstOrDefault();
if (window == null)
window = CreateInstance<SplineDataConversionWindow>();
window.titleContent = new GUIContent("Convert Spline Data Index Unit");
window.m_SplineDataProperty = splineDataProperty;
var(container, index) = FindPlausibleSplineContainer(targetComponent, splineDataProperty);
window.SetTargetSpline(container, index);
window.m_NewIndexUnit = (PathIndexUnit) newValue;
window.minSize = new Vector2(400, 124);
window.maxSize = new Vector2(400, 124);
if (forceShowDialog || container == null || index < 0)
{
window.ShowUtility();
}
else
{
window.ApplyConversion();
DestroyImmediate(window);
}
}
void SetTargetSpline(SplineContainer container, int index)
{
m_TargetContainer = container;
var splines = container == null ? Array.Empty<Spline>() : container.Splines;
m_TargetSpline = math.min(math.max(0, index), splines.Count-1);
m_SplineSelectionContent = new string[splines.Count];
m_SplineSelectionValues = new int[splines.Count];
for (int i = 0, c = splines.Count; i < c; ++i)
{
m_SplineSelectionContent[i] = $"Spline {i}";
m_SplineSelectionValues[i] = i;
}
}
void OnGUI()
{
GUILayout.Label(L10n.Tr("Spline Data Conversion"), EditorStyles.boldLabel);
if(m_TargetContainer == null)
EditorGUILayout.HelpBox(k_SplineDataConversionMessage,MessageType.Warning);
else
EditorGUILayout.HelpBox(L10n.Tr($"The spline {m_TargetContainer} will be used for data conversion."), MessageType.Info);
var container = m_TargetContainer;
container = (SplineContainer)EditorGUILayout.ObjectField("Reference Spline",
container,
typeof(SplineContainer),
true);
if(container != m_TargetContainer)
SetTargetSpline(container, m_TargetSpline);
m_TargetSpline = SplineGUILayout.SplineIndexPopup("Spline", m_TargetSpline, m_TargetContainer);
EditorGUILayout.BeginHorizontal();
if(GUILayout.Button(new GUIContent(L10n.Tr("Convert"), L10n.Tr("Convert data indexes to the new Unit."))))
{
if(m_TargetContainer != null)
ApplyConversion();
Close();
}
if(GUILayout.Button(L10n.Tr("Cancel")))
Close();
EditorGUILayout.EndHorizontal();
}
static (SplineContainer, int) FindPlausibleSplineContainer(Component targetComponent, SerializedProperty prop)
{
SplineContainer container = null;
var fieldInfos = targetComponent.GetType().GetFields(BindingFlags.Instance|BindingFlags.Public|BindingFlags.NonPublic);
var providerFieldInfo = fieldInfos.FirstOrDefault(field => field.FieldType == typeof(SplineContainer));
if(providerFieldInfo != null && providerFieldInfo.FieldType == typeof(SplineContainer))
container = (SplineContainer)providerFieldInfo.GetValue(targetComponent);
if(container == null)
container = targetComponent.gameObject.GetComponent<SplineContainer>();
int index = 0;
// if this is embedded SplineData, try to extract the spline index (if more than one spline)
if (container != null && container.Splines.Count > 1)
{
// m_Splines.Array.data[0].m_ObjectData.m_Data.Array.data[0].Value.m_DataPoints
var match = k_EmbeddedSplineDataContainerIndex.Match(prop.propertyPath);
if (match.Success && int.TryParse(match.Value, out var i))
index = i;
}
return (container, index);
}
void ApplyConversion()
{
m_SplineDataProperty.serializedObject.Update();
ConvertPathUnit(m_SplineDataProperty, m_TargetContainer, m_TargetSpline, m_NewIndexUnit);
m_SplineDataProperty.serializedObject.ApplyModifiedProperties();
}
static void ConvertPathUnit(SerializedProperty splineData,
ISplineContainer container,
int splineIndex,
PathIndexUnit newIndexUnit)
{
var spline = container.Splines[splineIndex];
var transform = container is Component component
? component.transform.localToWorldMatrix
: Matrix4x4.identity;
using var native = new NativeSpline(spline, transform);
var array = splineData.FindPropertyRelative("m_DataPoints");
var pathUnit = splineData.FindPropertyRelative("m_IndexUnit");
var from = (PathIndexUnit) Enum.GetValues(typeof(PathIndexUnit)).GetValue(pathUnit.enumValueIndex);
for (int i = 0, c = array.arraySize; i < c; ++i)
{
var point = array.GetArrayElementAtIndex(i);
var index = point.FindPropertyRelative("m_Index");
index.floatValue = native.ConvertIndexUnit(index.floatValue, from, newIndexUnit);
}
pathUnit.enumValueIndex = (int) newIndexUnit;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 97db6026b4154fbf97b957dd2137810d
timeCreated: 1634919075

View File

@@ -0,0 +1,321 @@
using System;
using System.Linq;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
/// <summary>
/// Provides default handles to SplineData.
/// Call <see cref="DataPointHandles"/> in your Editor Tool to add default handles
/// for you to add, move, and remove SplineData's DataPoints along a spline.
/// </summary>
public static class SplineDataHandles
{
const float k_HandleSize = 0.15f;
const int k_PickRes = 2;
static int[] s_DataPointsIDs;
static int s_NewDataPointIndex = -1;
static float s_AddingDataPoint = float.NaN;
/// <summary>
/// Creates manipulation handles in the SceneView to add, move, and remove SplineData's DataPoints along a spline.
/// DataPoints of the targeted SplineData along a Spline. Left click on an empty location
/// on the spline adds a new DataPoint in the SplineData. Left click on an existing DataPoint
/// allows to move this point along the Spline while a right click on it allows to delete that DataPoint.
/// </summary>
/// <remarks>
/// Left-click an empty location on the spline to add a new DataPoint to the SplineData.
/// Left-click on a DataPoint to move the point along the Spline. Right-click a DataPoint to delete it.
/// </remarks>
/// <param name="spline">The Spline to use to interprete the SplineData.</param>
/// <param name="splineData">The SplineData for which the handles are drawn.</param>
/// <param name="useDefaultValueOnAdd">Either to use default value or closer DataPoint value when adding new DataPoint.</param>
/// <typeparam name="TSpline">The Spline type.</typeparam>
/// <typeparam name="TData">The type of data this data point stores.</typeparam>
public static void DataPointHandles<TSpline, TData>(
this TSpline spline,
SplineData<TData> splineData,
bool useDefaultValueOnAdd = false)
where TSpline : ISpline
{
spline.DataPointHandles(splineData, useDefaultValueOnAdd, 0);
}
/// <summary>
/// Creates manipulation handles in the Scene view that can be used to add, move, and remove SplineData's DataPoints along a spline.
/// </summary>
/// <remarks>
/// Left-click an empty location on the spline to add a new DataPoint to the SplineData.
/// Left-click and drag a DataPoint to move the point along the spline. Right-click a DataPoint to delete it.
/// </remarks>
/// <param name="spline">The spline to use to interprete the SplineData.</param>
/// <param name="splineData">The SplineData for which the handles are drawn.</param>
/// <param name="splineID">The ID for the spline.</param>
/// <param name="useDefaultValueOnAdd">Whether to use the default value or a closer DataPoint value when adding new DataPoint.</param>
/// <typeparam name="TSpline">The spline type.</typeparam>
/// <typeparam name="TData">The type of data this data point stores.</typeparam>
public static void DataPointHandles<TSpline, TData>(
this TSpline spline,
SplineData<TData> splineData,
bool useDefaultValueOnAdd,
int splineID = 0)
where TSpline : ISpline
{
var id = GUIUtility.GetControlID(FocusType.Passive);
DataPointAddHandle(id, spline, splineData, useDefaultValueOnAdd, splineID);
// Draw Default manipulation handles
DataPointMoveHandles(spline, splineData);
// Remove DataPoint functionality
TryRemoveDataPoint(splineData);
}
static void TryRemoveDataPoint<TData>(SplineData<TData> splineData)
{
var evt = Event.current;
//Remove data point only when not adding one and when using right click button
if(float.IsNaN(s_AddingDataPoint) && GUIUtility.hotControl == 0
&& evt.type == EventType.MouseDown && evt.button == 1
&& s_DataPointsIDs.Contains(HandleUtility.nearestControl))
{
var dataPointIndex = splineData.Indexes.ElementAt(Array.IndexOf(s_DataPointsIDs, HandleUtility.nearestControl));
splineData.RemoveDataPoint(dataPointIndex);
GUI.changed = true;
evt.Use();
}
}
static bool IsHotControl(float splineID)
{
return !float.IsNaN(s_AddingDataPoint) && splineID.Equals(s_AddingDataPoint);
}
static void DataPointAddHandle<TSpline, TData>(
int controlID,
TSpline spline,
SplineData<TData> splineData,
bool useDefaultValueOnAdd,
int splineID)
where TSpline : ISpline
{
Event evt = Event.current;
EventType eventType = evt.GetTypeForControl(controlID);
switch (eventType)
{
case EventType.Layout:
{
if (!SplineHandles.ViewToolActive())
{
var ray = HandleUtility.GUIPointToWorldRay(evt.mousePosition);
SplineUtility.GetNearestPoint(spline, ray, out var pos, out _);
HandleUtility.AddControl(controlID, HandleUtility.DistanceToCircle(pos, 0.1f));
}
break;
}
case EventType.Repaint:
if ((HandleUtility.nearestControl == controlID && GUIUtility.hotControl == 0 && float.IsNaN(s_AddingDataPoint)) || IsHotControl(splineID))
{
var ray = HandleUtility.GUIPointToWorldRay(evt.mousePosition);
SplineUtility.GetNearestPoint(spline, ray, out var pos, out var t);
var upDir = spline.EvaluateUpVector(t);
Handles.CircleHandleCap(controlID, pos, Quaternion.LookRotation(upDir), 0.15f * HandleUtility.GetHandleSize(pos), EventType.Repaint);
}
break;
case EventType.MouseDown:
if (evt.button == 0
&& !SplineHandles.ViewToolActive()
&& HandleUtility.nearestControl == controlID
&& GUIUtility.hotControl == 0)
{
s_AddingDataPoint = splineID;
var ray = HandleUtility.GUIPointToWorldRay(evt.mousePosition);
SplineUtility.GetNearestPoint(spline, ray, out _, out var t);
var index = SplineUtility.ConvertIndexUnit(
spline, t,
splineData.PathIndexUnit);
s_NewDataPointIndex = splineData.AddDataPointWithDefaultValue(index, useDefaultValueOnAdd);
GUI.changed = true;
evt.Use();
}
break;
case EventType.MouseDrag:
if (evt.button == 0 && IsHotControl(splineID))
{
var ray = HandleUtility.GUIPointToWorldRay(evt.mousePosition);
SplineUtility.GetNearestPoint(spline, ray, out _, out var t);
var index = SplineUtility.ConvertIndexUnit(
spline, t,
splineData.PathIndexUnit);
s_NewDataPointIndex = splineData.MoveDataPoint(s_NewDataPointIndex, index);
GUI.changed = true;
evt.Use();
}
break;
case EventType.MouseUp:
if (evt.button == 0 && IsHotControl(splineID))
{
s_AddingDataPoint = float.NaN;
s_NewDataPointIndex = -1;
GUIUtility.hotControl = 0;
evt.Use();
}
break;
case EventType.MouseMove:
HandleUtility.Repaint();
break;
}
}
static void DataPointMoveHandles<TSpline, TData>(TSpline spline, SplineData<TData> splineData)
where TSpline : ISpline
{
if(s_DataPointsIDs == null || s_DataPointsIDs.Length != splineData.Count)
s_DataPointsIDs = new int[splineData.Count];
//Cache all data point IDs
for(int dataIndex = 0; dataIndex < splineData.Count; dataIndex++)
s_DataPointsIDs[dataIndex] = GUIUtility.GetControlID(FocusType.Passive);
//Draw all data points handles on the spline
for(int dataIndex = 0; dataIndex < splineData.Count; dataIndex++)
{
var index = splineData.Indexes.ElementAt(dataIndex);
SplineDataHandle(
s_DataPointsIDs[dataIndex],
spline,
splineData,
index,
k_HandleSize,
out float newIndex);
if(GUIUtility.hotControl == s_DataPointsIDs[dataIndex])
{
var newDataIndex = splineData.MoveDataPoint(dataIndex, newIndex);
// If the current DataPoint is moved across another DataPoint, then update the hotControl ID
if(newDataIndex - index != 0)
GUIUtility.hotControl = s_DataPointsIDs[newDataIndex];
}
}
}
static void SplineDataHandle<TSpline, TData>(
int controlID,
TSpline spline,
SplineData<TData> splineData,
float dataPointIndex,
float size,
out float newTime) where TSpline : ISpline
{
newTime = dataPointIndex;
Event evt = Event.current;
EventType eventType = evt.GetTypeForControl(controlID);
var normalizedT = SplineUtility.GetNormalizedInterpolation(spline, dataPointIndex, splineData.PathIndexUnit);
var dataPosition = SplineUtility.EvaluatePosition(spline, normalizedT);
switch (eventType)
{
case EventType.Layout:
var dist = HandleUtility.DistanceToCircle(dataPosition, size * HandleUtility.GetHandleSize(dataPosition));
HandleUtility.AddControl(controlID, dist);
break;
case EventType.Repaint:
DrawSplineDataHandle(controlID, dataPosition, size);
break;
case EventType.MouseDown:
if (evt.button == 0
&& !SplineHandles.ViewToolActive()
&& HandleUtility.nearestControl == controlID
&& GUIUtility.hotControl == 0)
{
GUIUtility.hotControl = controlID;
newTime = GetClosestSplineDataT(spline, splineData);
GUI.changed = true;
evt.Use();
}
break;
case EventType.MouseDrag:
if (GUIUtility.hotControl == controlID)
{
newTime = GetClosestSplineDataT(spline, splineData);
GUI.changed = true;
evt.Use();
}
break;
case EventType.MouseUp:
if (GUIUtility.hotControl == controlID)
{
if(evt.button == 0)
{
GUIUtility.hotControl = 0;
newTime = GetClosestSplineDataT(spline, splineData);
}
evt.Use();
}
break;
case EventType.MouseMove:
HandleUtility.Repaint();
break;
}
}
static void DrawSplineDataHandle(int controlID, Vector3 position, float size)
{
var handleColor = Handles.color;
if(controlID == GUIUtility.hotControl)
handleColor = Handles.selectedColor;
else if(GUIUtility.hotControl == 0 && controlID == HandleUtility.nearestControl)
handleColor = Handles.preselectionColor;
// to avoid affecting the sphere dimensions with the handles matrix, we'll just use the position and reset
// the matrix to identity when drawing.
position = Handles.matrix * position;
using(new Handles.DrawingScope(handleColor, Matrix4x4.identity))
{
Handles.SphereHandleCap(
controlID,
position,
Quaternion.identity,
size * HandleUtility.GetHandleSize(position),
EventType.Repaint
);
}
}
// Spline must be in world space
static float GetClosestSplineDataT<TSpline,TData>(TSpline spline, SplineData<TData> splineData) where TSpline : ISpline
{
var evt = Event.current;
var ray = HandleUtility.GUIPointToWorldRay(evt.mousePosition);
SplineUtility.GetNearestPoint(spline,
ray,
out float3 _,
out float t,
k_PickRes);
return SplineUtility.ConvertIndexUnit(spline, t, splineData.PathIndexUnit);
}
}
}

View File

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

View File

@@ -0,0 +1,207 @@
using System;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
/// <summary>
/// Contains IMGUI controls for editing Spline data.
/// </summary>
public static class SplineGUI
{
static GUIContent s_TempContent = new GUIContent();
static string[] s_SplineIndexPopupContents = new string[1] { "Spline 0" };
internal static GUIContent TempContent(string label, string tooltip = null, Texture2D image = null)
{
s_TempContent.text = label;
s_TempContent.tooltip = tooltip;
s_TempContent.image = image;
return s_TempContent;
}
/// <summary>
/// Creates a dropdown to select an index between 0 and the count of <see cref="Splines"/> contained in the
/// provided <paramref name="container"/>.
/// </summary>
/// <param name="container">A <see cref="SplineContainer"/> that determines how many splines are available in
/// the popup selector.</param>
/// <param name="label">The label to use for this property. If null, the property display name is used.</param>
/// <param name="rect">The rectangle on the screen to use for the field.</param>
/// <param name="property">A SerializedProperty that stores an integer value.</param>
/// <exception cref="ArgumentException">An exception is thrown if <paramref name="property"/> is not an integer
/// field.</exception>
/// <typeparam name="T">The type implementing <see cref="ISplineContainer"/>.</typeparam>
public static void SplineIndexField<T>(Rect rect, SerializedProperty property, GUIContent label, T container) where T : ISplineContainer
{
SplineIndexField(rect, property, label, container == null ? 0 : container.Splines.Count);
}
/// <summary>
/// Creates a dropdown to select an index between 0 and <paramref name="splineCount"/>.
/// </summary>
/// <param name="rect">The rectangle on the screen to use for the field.</param>
/// <param name="property">A SerializedProperty that stores an integer value.</param>
/// <param name="label">The label to use for this property. If null, the property display name is used.</param>
/// <param name="splineCount">The number of splines available. In most cases, this is the size of
/// <see cref="SplineContainer.Splines"/></param>
/// <exception cref="ArgumentException">An exception is thrown if <paramref name="property"/> is not an integer
/// field.</exception>
public static void SplineIndexField(Rect rect, SerializedProperty property, GUIContent label, int splineCount)
{
if (property.propertyType != SerializedPropertyType.Integer)
throw new ArgumentException("Spline index property must be of type `int`.", nameof(property));
EditorGUI.showMixedValue = property.hasMultipleDifferentValues;
property.intValue = SplineIndexPopup(rect, label == null ? property.displayName : label.text, property.intValue, splineCount);
EditorGUI.showMixedValue = false;
}
/// <summary>
/// Creates a dropdown to select an index between 0 and <paramref name="splineCount"/>.
/// </summary>
/// <param name="label">An optional prefix label.</param>
/// <param name="splineCount">The number of splines available. In most cases, this is the size of
/// <see cref="SplineContainer.Splines"/></param>
/// <param name="rect">The rectangle on the screen to use for the field.</param>
/// <param name="index">The current index.</param>
/// <returns>The selected index.</returns>
public static int SplineIndexPopup(Rect rect, string label, int index, int splineCount)
{
if (splineCount != s_SplineIndexPopupContents.Length)
{
Array.Resize(ref s_SplineIndexPopupContents, splineCount);
for (int i = 0; i < splineCount; ++i)
s_SplineIndexPopupContents[i] = $"Spline {i}";
}
return Math.Min(
Math.Max(0, EditorGUI.IntPopup(rect, label, index, s_SplineIndexPopupContents, null)),
splineCount-1);
}
}
/// <summary>
/// Provides IMGUI controls to edit Spline data.
/// </summary>
public static class SplineGUILayout
{
/// <summary>
/// Creates a dropdown to select an index between 0 and the count of <see cref="Splines"/> contained in the
/// provided <paramref name="container"/>.
/// </summary>
/// <param name="container">A <see cref="SplineContainer"/> that determines how many splines are available in
/// the popup selector.</param>
/// <param name="property">A SerializedProperty that stores an integer value.</param>
/// <exception cref="ArgumentException">An exception is thrown if <paramref name="property"/> is not an integer
/// field.</exception>
/// <typeparam name="T">The type implementing <see cref="ISplineContainer"/>.</typeparam>
public static void SplineIndexField<T>(SerializedProperty property, T container) where T : ISplineContainer
{
SplineIndexField(property, container == null ? 0 : container.Splines.Count);
}
/// <summary>
/// Creates a dropdown to select an index between 0 and <paramref name="splineCount"/>.
/// </summary>
/// <param name="property">A SerializedProperty that stores an integer value.</param>
/// <param name="splineCount">The number of splines available. In most cases, this is the size of
/// <see cref="SplineContainer.Splines"/></param>
/// <exception cref="ArgumentException">An exception is thrown if <paramref name="property"/> is not an integer
/// field.</exception>
public static void SplineIndexField(SerializedProperty property, int splineCount)
{
if (property.propertyType != SerializedPropertyType.Integer)
throw new ArgumentException("Spline index property must be of type `int`.", nameof(property));
EditorGUI.showMixedValue = property.hasMultipleDifferentValues;
property.intValue = SplineIndexPopup(property.displayName, property.intValue, splineCount);
EditorGUI.showMixedValue = false;
}
/// <summary>
/// Creates a dropdown to select a spline index relative to <paramref name="container"/>.
/// </summary>
/// <param name="label">An optional prefix label.</param>
/// <param name="index">The current index.</param>
/// <param name="container">A <see cref="SplineContainer"/> that determines how many splines are available in
/// the popup selector.</param>
/// <typeparam name="T">The type of <see cref="ISplineContainer"/>.</typeparam>
/// <returns>The selected index.</returns>
public static int SplineIndexPopup<T>(string label, int index, T container) where T : ISplineContainer
{
return SplineIndexPopup(label, index, container == null ? 0 : container.Splines.Count);
}
/// <summary>
/// Creates a dropdown to select an index between 0 and <paramref name="splineCount"/>.
/// </summary>
/// <param name="label">An optional prefix label.</param>
/// <param name="splineCount">The number of splines available. In most cases, this is the size of
/// <see cref="SplineContainer.Splines"/></param>
/// <param name="index">The current index.</param>
/// <returns>The selected index.</returns>
public static int SplineIndexPopup(string label, int index, int splineCount)
{
var rect = GUILayoutUtility.GetRect(SplineGUI.TempContent(label), EditorStyles.popup);
return SplineGUI.SplineIndexPopup(rect, label, index, splineCount);
}
/// <summary>
/// Creates a field for an embedded <see cref="SplineData{T}"/> property. Embedded <see cref="SplineData{T}"/>
/// is stored in the <see cref="Spline"/> class and can be accessed through a string key value. Use this
/// function to expose an embedded <see cref="SplineData{T}"/> through the Inspector.
/// </summary>
/// <param name="container">The <see cref="SplineContainer"/> that holds the <see cref="Spline"/> target.</param>
/// <param name="index">The index of the target <see cref="Spline"/> in the <see cref="SplineContainer.Splines"/>
/// array.</param>
/// <param name="type">The <see cref="EmbeddedSplineDataType"/> type of data stored in the embedded
/// <see cref="SplineData{T}"/></param>
/// <param name="key">A string value used to identify and access embedded <see cref="SplineData{T}"/>.</param>
/// <returns>True if the property has children, is expanded, and includeChildren was set to false. Returns false otherwise.</returns>
public static bool EmbeddedSplineDataField(SplineContainer container, int index, EmbeddedSplineDataType type, string key)
{
return EmbeddedSplineDataField(null, container, index, type, key);
}
/// <summary>
/// Creates a field for an embedded <see cref="SplineData{T}"/> property. Embedded <see cref="SplineData{T}"/>
/// is stored in the <see cref="Spline"/> class and can be accessed through a string key value. Use this
/// function to expose an embedded <see cref="SplineData{T}"/> through the Inspector.
/// </summary>
/// <param name="label">An optional prefix label.</param>
/// <param name="container">The <see cref="SplineContainer"/> that holds the <see cref="Spline"/> target.</param>
/// <param name="index">The index of the target <see cref="Spline"/> in the <see cref="SplineContainer.Splines"/>
/// array.</param>
/// <param name="type">The <see cref="EmbeddedSplineDataType"/> type of data stored in the embedded
/// <see cref="SplineData{T}"/></param>
/// <param name="key">A string value used to identify and access embedded <see cref="SplineData{T}"/>.</param>
/// <returns>True if the property has children, is expanded, and includeChildren was set to false. Returns false otherwise.</returns>
public static bool EmbeddedSplineDataField(GUIContent label,
SplineContainer container,
int index,
EmbeddedSplineDataType type,
string key)
{
if (container == null || index < 0 || index >= container.Splines.Count)
return false;
var property = SerializedPropertyUtility.GetEmbeddedSplineDataProperty(container, index, type, key);
property.serializedObject.Update();
EditorGUI.BeginChangeCheck();
var ret = EditorGUILayout.PropertyField(property, label);
if(EditorGUI.EndChangeCheck())
property.serializedObject.ApplyModifiedProperties();
return ret;
}
internal static void QuaternionField(Rect rect, GUIContent content, SerializedProperty property)
{
EditorGUI.BeginChangeCheck();
Quaternion value = SplineGUIUtility.GetQuaternionValue(property);
var result = EditorGUI.Vector3Field(rect, content, value.eulerAngles);
if (EditorGUI.EndChangeCheck())
SplineGUIUtility.SetQuaternionValue(property, Quaternion.Euler(result));
}
}
}

View File

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

View File

@@ -0,0 +1,82 @@
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
static class SplineGUIUtility
{
public delegate bool EqualityComparer<in T>(T a, T b);
internal static readonly float lineHeight = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
public static bool HasMultipleValues<T>(IReadOnlyList<T> elements, EqualityComparer<T> comparer)
{
if (elements.Count < 2)
return false;
var first = elements[0];
for (int i = 1; i < elements.Count; ++i)
if (!comparer.Invoke(first, elements[i]))
return true;
return false;
}
public static quaternion GetQuaternionValue(SerializedProperty property)
{
return new quaternion(
property.FindPropertyRelative("value.x").floatValue,
property.FindPropertyRelative("value.y").floatValue,
property.FindPropertyRelative("value.z").floatValue,
property.FindPropertyRelative("value.w").floatValue);
}
public static void SetQuaternionValue(SerializedProperty property, Quaternion value)
{
property.FindPropertyRelative("value.x").floatValue = value.x;
property.FindPropertyRelative("value.y").floatValue = value.y;
property.FindPropertyRelative("value.z").floatValue = value.z;
property.FindPropertyRelative("value.w").floatValue = value.w;
}
public static SerializedProperty GetParentSplineProperty(SerializedProperty property)
{
var properties = property.propertyPath.Split('.');
if (properties.Length == 0)
return null;
var current = property.serializedObject.FindProperty(properties[0]);
for (var i = 1; i < properties.Length; ++i)
{
var p = properties[i];
if (current.type == typeof(Spline).Name)
return current;
if (current.propertyType == SerializedPropertyType.ManagedReference
&& current.managedReferenceFullTypename == typeof(Spline).AssemblyQualifiedName)
return current;
current = current.FindPropertyRelative(p);
}
return null;
}
public static Rect ReserveSpace(float height, ref Rect total)
{
Rect current = total;
current.height = height;
total.y += height;
return current;
}
public static Rect ReserveSpaceForLine(ref Rect total)
{
var height = EditorGUIUtility.wideMode ? lineHeight : 2f * lineHeight;
return ReserveSpace(height, ref total);
}
}
}

View File

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

View File

@@ -0,0 +1,53 @@
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
/// <summary>
/// A PropertyDrawer used to show a popup menu with available spline indices relative to a <see cref="ISplineContainer"/>.
/// Add <see cref="UnityEngine.Splines.SplineIndexAttribute"/> to a serialized integer type to use.
/// </summary>
[CustomPropertyDrawer(typeof(SplineIndexAttribute))]
public class SplineIndexPropertyDrawer : PropertyDrawer
{
static readonly int[] k_MissingContainerValues = new int[] { 0 };
static readonly GUIContent[] k_MissingContainerContent = new GUIContent[] { new GUIContent("") };
string GetWarningMessage(SplineIndexAttribute attrib) =>
$"SplineIndex property attribute does not reference a valid SplineContainer: \"{attrib.SplineContainerProperty}\"";
/// <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;
}
/// <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)
{
if (property.propertyType != SerializedPropertyType.Integer || attribute is not SplineIndexAttribute attrib)
return;
var path = property.propertyPath.Replace(property.name, attrib.SplineContainerProperty);
var container = property.serializedObject.FindProperty(path);
if (container == null || !(container.objectReferenceValue is ISplineContainer res))
{
new EditorGUI.DisabledScope(true);
EditorGUI.IntPopup(position, label, 0, k_MissingContainerContent, k_MissingContainerValues);
}
else
SplineGUI.SplineIndexField(position, property, label, res.Splines.Count);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5d65cf8364d644f1a82a62a1c0272310
timeCreated: 1671553991

View File

@@ -0,0 +1,83 @@
using Unity.Mathematics;
using UnityEngine.Splines;
using UnityEngine;
using UnityEditor.EditorTools;
namespace UnityEditor.Splines
{
static class SplineMenu
{
const string k_MenuPath = "GameObject/Spline";
internal static GameObject CreateSplineGameObject(MenuCommand menuCommand, Spline spline = null)
{
var name = GameObjectUtility.GetUniqueNameForSibling(null, "Spline");
var gameObject = ObjectFactory.CreateGameObject(name, typeof(SplineContainer));
#if UNITY_2022_1_OR_NEWER
ObjectFactory.PlaceGameObject(gameObject, menuCommand.context as GameObject);
#else
if (menuCommand.context is GameObject go)
{
Undo.RecordObject(gameObject.transform, "Re-parenting");
gameObject.transform.SetParent(go.transform);
}
#endif
if (spline != null)
{
var container = gameObject.GetComponent<SplineContainer>();
container.Spline = spline;
}
Selection.activeGameObject = gameObject;
return gameObject;
}
const int k_MenuPriority = 10;
[MenuItem(k_MenuPath + "/Draw Splines Tool...", false, k_MenuPriority + 0)]
static void CreateNewSpline(MenuCommand menuCommand)
{
ToolManager.SetActiveTool<CreateSplineTool>();
}
[MenuItem(k_MenuPath + "/Square", false, k_MenuPriority + 11)]
static void CreateSquare(MenuCommand command)
{
CreateSplineGameObject(command, SplineFactory.CreateSquare(1f));
}
[MenuItem(k_MenuPath + "/Rounded Square", false, k_MenuPriority + 12)]
static void CreateRoundedSquare(MenuCommand command)
{
CreateSplineGameObject(command, SplineFactory.CreateRoundedCornerSquare(1f, 0.25f));
}
[MenuItem(k_MenuPath + "/Circle", false, k_MenuPriority + 13)]
static void CreateCircle(MenuCommand command)
{
CreateSplineGameObject(command, SplineFactory.CreateCircle(0.5f));
}
[MenuItem(k_MenuPath + "/Polygon", false, k_MenuPriority + 14)]
static void CreatePolygon(MenuCommand command)
{
var edgeSize = math.sin(math.PI / 6f);
CreateSplineGameObject(command, SplineFactory.CreatePolygon(edgeSize, 6));
}
[MenuItem(k_MenuPath + "/Helix", false, k_MenuPriority + 15)]
static void CreateHelix(MenuCommand command)
{
CreateSplineGameObject(command, SplineFactory.CreateHelix(0.5f, 1f, 1));
}
[MenuItem(k_MenuPath + "/Star", false, k_MenuPriority + 16)]
static void CreateStar(MenuCommand command)
{
var edgeSize = math.sin(math.PI / 5f);
CreateSplineGameObject(command, SplineFactory.CreateStarPolygon(edgeSize, 5, 0.5f));
}
}
}

View File

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e21391b51918435b9e07d2f917a37c79
timeCreated: 1675955599

View File

@@ -0,0 +1,113 @@
using UnityEngine;
using UnityEngine.UIElements;
using UnityEngine.Splines;
#if !UNITY_2022_1_OR_NEWER
using UnityEditor.UIElements;
#endif
namespace UnityEditor.Splines
{
sealed class BezierKnotDrawer : ElementDrawer<SelectableKnot>
{
static readonly string k_PositionTooltip = L10n.Tr("Knot Position");
static readonly string k_RotationTooltip = L10n.Tr("Knot Rotation");
readonly Float3PropertyField<SelectableKnot> m_Position;
readonly Float3PropertyField<SelectableKnot> m_Rotation;
readonly TangentModePropertyField<SelectableKnot> m_Mode;
readonly BezierTangentPropertyField<SelectableKnot> m_BezierMode;
readonly TangentPropertyField m_TangentIn;
readonly TangentPropertyField m_TangentOut;
public BezierKnotDrawer()
{
VisualElement row;
Add(row = new VisualElement(){name = "Vector3WithIcon"});
row.tooltip = k_PositionTooltip;
row.style.flexDirection = FlexDirection.Row;
row.Add(new VisualElement(){name = "PositionIcon"});
row.Add(m_Position = new Float3PropertyField<SelectableKnot>("",
(knot) => knot.LocalPosition,
(knot, value) => knot.LocalPosition = value)
{ name = "Position" });
m_Position.style.flexGrow = 1;
Add(row = new VisualElement(){name = "Vector3WithIcon"});
row.tooltip = k_RotationTooltip;
row.style.flexDirection = FlexDirection.Row;
row.Add(new VisualElement(){name = "RotationIcon"});
row.Add(m_Rotation = new Float3PropertyField<SelectableKnot>("",
(knot) => ((Quaternion)knot.LocalRotation).eulerAngles,
(knot, value) => knot.LocalRotation = Quaternion.Euler(value))
{ name = "Rotation" });
m_Rotation.style.flexGrow = 1;
Add(new Separator());
Add(m_Mode = new TangentModePropertyField<SelectableKnot>());
m_Mode.changed += Update;
Add(m_BezierMode = new BezierTangentPropertyField<SelectableKnot>());
m_BezierMode.changed += Update;
Add(m_TangentIn = new TangentPropertyField("In", "TangentIn", BezierTangent.In));
Add(m_TangentOut = new TangentPropertyField("Out", "TangentOut", BezierTangent.Out));
//Update opposite to take into account some tangent modes
m_TangentIn.changed += () => m_TangentOut.Update(targets);
m_TangentOut.changed += () => m_TangentIn.Update(targets);
}
public override string GetLabelForTargets()
{
if (targets.Count > 1)
return $"<b>({targets.Count}) Knots</b> selected";
return $"<b>Knot {target.KnotIndex}</b> (<b>Spline {target.SplineInfo.Index}</b>) selected";
}
public override void Update()
{
base.Update();
m_Position.Update(targets);
m_Rotation.Update(targets);
m_Mode.Update(targets);
m_BezierMode.Update(targets);
m_TangentIn.Update(targets);
m_TangentOut.Update(targets);
//Disabling edition when using linear tangents
UpdateTangentsState();
}
void UpdateTangentsState()
{
bool tangentsModifiable = true;
bool tangentsBroken = true;
bool tangentInSelectable = false;
bool tangentOutSelectable = false;
for (int i = 0; i < targets.Count; ++i)
{
var mode = targets[i].Mode;
tangentsModifiable &= SplineUtility.AreTangentsModifiable(mode);
tangentsBroken &= mode == TangentMode.Broken;
tangentInSelectable |= SplineSelectionUtility.IsSelectable(targets[i].TangentIn);
tangentOutSelectable |= SplineSelectionUtility.IsSelectable(targets[i].TangentOut);
}
m_TangentIn.SetEnabled(tangentsModifiable && tangentInSelectable);
m_TangentOut.SetEnabled(tangentsModifiable && tangentOutSelectable);
if(tangentsModifiable)
{
m_TangentIn.vector3field.SetEnabled(tangentsBroken);
m_TangentOut.vector3field.SetEnabled(tangentsBroken);
}
}
}
}

View File

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

View File

@@ -0,0 +1,64 @@
using UnityEditor.Toolbars;
using UnityEngine;
using UnityEngine.Splines;
#if !UNITY_2022_1_OR_NEWER
#endif
namespace UnityEditor.Splines.Editor.GUI
{
[EditorToolbarElement("Spline Tool Settings/Default Knot Type")]
class DefaultKnotTypeDropdown : EditorToolbarDropdown
{
const string k_LinearIconPath = "Packages/com.unity.splines/Editor/Resources/Icons/Tangent_Linear.png";
const string k_AutoSmoothIconPath = "Packages/com.unity.splines/Editor/Resources/Icons/AutoSmoothKnot.png";
readonly GUIContent[] m_OptionContents = new GUIContent[2];
public DefaultKnotTypeDropdown()
{
name = "Default Knot Type";
var content = EditorGUIUtility.TrTextContent("Linear",
"Tangents are not used. A linear knot tries to connect to another by a path with no curvature.",
k_LinearIconPath);
m_OptionContents[0] = content;
content = EditorGUIUtility.TrTextContent("Auto Smooth",
"Tangents are calculated using the previous and next knot positions.",
k_AutoSmoothIconPath);
m_OptionContents[1] = content;
clicked += OpenContextMenu;
RefreshElementContent();
}
void OpenContextMenu()
{
var menu = new GenericMenu();
menu.AddItem(m_OptionContents[0], EditorSplineUtility.DefaultTangentMode == TangentMode.Linear,
() => SetTangentModeIfNeeded(TangentMode.Linear));
menu.AddItem(m_OptionContents[1], EditorSplineUtility.DefaultTangentMode == TangentMode.AutoSmooth,
() => SetTangentModeIfNeeded(TangentMode.AutoSmooth));
menu.DropDown(worldBound);
}
void SetTangentModeIfNeeded(TangentMode tangentMode)
{
if (EditorSplineUtility.DefaultTangentMode != tangentMode)
{
EditorSplineUtility.s_DefaultTangentMode.SetValue(tangentMode, true);
RefreshElementContent();
}
}
void RefreshElementContent()
{
var content = m_OptionContents[EditorSplineUtility.DefaultTangentMode == TangentMode.Linear ? 0 : 1];
text = content.text;
tooltip = content.tooltip;
icon = content.image as Texture2D;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: facf982a205a4c93830bb62332a75ceb
timeCreated: 1649792821

View File

@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine.UIElements;
#if !UNITY_2022_1_OR_NEWER
using UnityEditor.UIElements;
#endif
namespace UnityEditor.Splines
{
class Float3PropertyField<T> : Vector3Field
where T : ISelectableElement
{
static readonly List<float3> s_Float3Buffer = new List<float3>();
static readonly SplineGUIUtility.EqualityComparer<float3> s_ComparerX = (a, b) => a.x.Equals(b.x);
static readonly SplineGUIUtility.EqualityComparer<float3> s_ComparerY = (a, b) => a.y.Equals(b.y);
static readonly SplineGUIUtility.EqualityComparer<float3> s_ComparerZ = (a, b) => a.z.Equals(b.z);
readonly FloatField m_X;
readonly FloatField m_Y;
readonly FloatField m_Z;
readonly Func<T, float3> m_Get;
readonly Action<T, float3> m_Set;
IReadOnlyList<T> m_Elements = new List<T>(0);
public event Action changed;
public Float3PropertyField(string label, Func<T, float3> get, Action<T, float3> set) : base(label)
{
m_Get = get;
m_Set = set;
m_X = this.Q<FloatField>("unity-x-input");
m_Y = this.Q<FloatField>("unity-y-input");
m_Z = this.Q<FloatField>("unity-z-input");
m_X.RegisterValueChangedCallback(ApplyX);
m_Y.RegisterValueChangedCallback(ApplyY);
m_Z.RegisterValueChangedCallback(ApplyZ);
}
public void Update(IReadOnlyList<T> elements)
{
m_Elements = elements;
s_Float3Buffer.Clear();
for (int i = 0; i < elements.Count; ++i)
s_Float3Buffer.Add(m_Get.Invoke(elements[i]));
var value = s_Float3Buffer.Count > 0 ? s_Float3Buffer[0] : 0;
m_X.showMixedValue = SplineGUIUtility.HasMultipleValues(s_Float3Buffer, s_ComparerX);
if (!m_X.showMixedValue)
m_X.SetValueWithoutNotify(value[0]);
m_Y.showMixedValue = SplineGUIUtility.HasMultipleValues(s_Float3Buffer, s_ComparerY);
if (!m_Y.showMixedValue)
m_Y.SetValueWithoutNotify(value[1]);
m_Z.showMixedValue = SplineGUIUtility.HasMultipleValues(s_Float3Buffer, s_ComparerZ);
if (!m_Z.showMixedValue)
m_Z.SetValueWithoutNotify(value[2]);
}
void ApplyX(ChangeEvent<float> evt)
{
EditorSplineUtility.RecordObjects(m_Elements, SplineInspectorOverlay.SplineChangeUndoMessage);
ElementInspector.ignoreKnotCallbacks = true;
for (int i = 0; i < m_Elements.Count; ++i)
{
var value = m_Get.Invoke(m_Elements[i]);
value.x = evt.newValue;
m_Set.Invoke(m_Elements[i], value);
}
m_X.showMixedValue = false;
m_X.SetValueWithoutNotify(evt.newValue);
changed?.Invoke();
ElementInspector.ignoreKnotCallbacks = false;
}
void ApplyY(ChangeEvent<float> evt)
{
EditorSplineUtility.RecordObjects(m_Elements, SplineInspectorOverlay.SplineChangeUndoMessage);
ElementInspector.ignoreKnotCallbacks = true;
for (int i = 0; i < m_Elements.Count; ++i)
{
var value = m_Get.Invoke(m_Elements[i]);
value.y = evt.newValue;
m_Set.Invoke(m_Elements[i], value);
}
m_Y.showMixedValue = false;
m_Y.SetValueWithoutNotify(evt.newValue);
changed?.Invoke();
ElementInspector.ignoreKnotCallbacks = false;
}
void ApplyZ(ChangeEvent<float> evt)
{
EditorSplineUtility.RecordObjects(m_Elements, SplineInspectorOverlay.SplineChangeUndoMessage);
ElementInspector.ignoreKnotCallbacks = true;
for (int i = 0; i < m_Elements.Count; ++i)
{
var value = m_Get.Invoke(m_Elements[i]);
value.z = evt.newValue;
m_Set.Invoke(m_Elements[i], value);
}
m_Z.showMixedValue = false;
m_Z.SetValueWithoutNotify(evt.newValue);
changed?.Invoke();
ElementInspector.ignoreKnotCallbacks = false;
}
}
}

View File

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

View File

@@ -0,0 +1,94 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.Toolbars;
namespace UnityEditor.Splines
{
[EditorToolbarElement("Spline Tool Settings/Handle Rotation")]
class HandleRotationDropdown : EditorToolbarDropdown
{
const string k_ParentRotationIconPath = "Packages/com.unity.splines/Editor/Resources/Icons/ToolHandleParent.png";
const string k_ElementRotationIconPath = "Packages/com.unity.splines/Editor/Resources/Icons/ToolHandleElement.png";
readonly List<GUIContent> m_OptionContents = new List<GUIContent>();
public HandleRotationDropdown()
{
name = "Handle Rotation";
var content = EditorGUIUtility.TrTextContent("Local",
"Toggle Tool Handle Rotation\n\nTool handles are in the active object's rotation.",
"ToolHandleLocal");
m_OptionContents.Add(content);
content = EditorGUIUtility.TrTextContent("Global",
"Toggle Tool Handle Rotation\n\nTool handles are in global rotation.",
"ToolHandleGlobal");
m_OptionContents.Add(content);
content = EditorGUIUtility.TrTextContent("Parent",
"Toggle Tool Handle Rotation\n\nTool handles are in active element's parent's rotation.",
k_ParentRotationIconPath);
m_OptionContents.Add(content);
content = EditorGUIUtility.TrTextContent("Element",
"Toggle Tool Handle Rotation\n\nTool handles are in active element's rotation.",
k_ElementRotationIconPath);
m_OptionContents.Add(content);
RegisterCallback<AttachToPanelEvent>(AttachedToPanel);
RegisterCallback<DetachFromPanelEvent>(DetachedFromPanel);
clicked += OpenContextMenu;
RefreshElementContent();
}
void OpenContextMenu()
{
var menu = new GenericMenu();
menu.AddItem(m_OptionContents[(int)HandleOrientation.Global], SplineTool.handleOrientation == HandleOrientation.Global,
() => SetHandleOrientationIfNeeded(HandleOrientation.Global));
menu.AddItem(m_OptionContents[(int)HandleOrientation.Local], SplineTool.handleOrientation == HandleOrientation.Local,
() => SetHandleOrientationIfNeeded(HandleOrientation.Local));
menu.AddItem(m_OptionContents[(int)HandleOrientation.Parent], SplineTool.handleOrientation == HandleOrientation.Parent,
() => SetHandleOrientationIfNeeded(HandleOrientation.Parent));
menu.AddItem(m_OptionContents[(int)HandleOrientation.Element], SplineTool.handleOrientation == HandleOrientation.Element,
() => SetHandleOrientationIfNeeded(HandleOrientation.Element));
menu.DropDown(worldBound);
}
void SetHandleOrientationIfNeeded(HandleOrientation handleOrientation)
{
if (SplineTool.handleOrientation != handleOrientation)
{
SplineTool.handleOrientation = handleOrientation;
RefreshElementContent();
}
}
void RefreshElementContent()
{
var content = m_OptionContents[(int)SplineTool.handleOrientation];
text = content.text;
tooltip = content.tooltip;
icon = content.image as Texture2D;
}
void AttachedToPanel(AttachToPanelEvent evt)
{
SplineTool.handleOrientationChanged += RefreshElementContent;
}
void DetachedFromPanel(DetachFromPanelEvent evt)
{
SplineTool.handleOrientationChanged -= RefreshElementContent;
}
}
}

View File

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

View File

@@ -0,0 +1,12 @@
using UnityEngine.UIElements;
namespace UnityEditor.Splines
{
sealed class Separator : VisualElement
{
public Separator()
{
AddToClassList("unity-separator");
}
}
}

View File

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

View File

@@ -0,0 +1,35 @@
using UnityEditor.EditorTools;
using UnityEditor.Toolbars;
using UnityEngine;
using UnityEngine.UIElements;
namespace UnityEditor.Splines
{
[EditorToolbarElement("Spline Tool Settings/Handle Visuals")]
sealed class SplineHandleSettingsDropdown : EditorToolbarDropdown
{
public SplineHandleSettingsDropdown()
{
var content = EditorGUIUtility.TrTextContent("Visuals", "Visual settings for handles");
text = content.text;
tooltip = content.tooltip;
icon = (Texture2D)content.image;
clicked += OnClick;
RegisterCallback<AttachToPanelEvent>(AttachToPanel);
}
void OnClick()
{
SplineHandleSettingsWindow.Show(worldBound);
}
void AttachToPanel(AttachToPanelEvent evt)
{
var toolType = ToolManager.activeToolType;
SetEnabled(toolType != typeof(KnotPlacementTool));
}
}
}

View File

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

View File

@@ -0,0 +1,88 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace UnityEditor.Splines
{
sealed class SplineHandleSettingsWindow : EditorWindow
{
const float k_BorderWidth = 1;
Toggle m_FlowDirection;
Toggle m_AllTangents;
Toggle m_KnotIndices;
Toggle m_SplineMesh;
public static void Show(Rect buttonRect)
{
var window = CreateInstance<SplineHandleSettingsWindow>();
window.hideFlags = HideFlags.DontSave;
#if UNITY_2022_1_OR_NEWER
var popupWidth = 150;
#else
var popupWidth = 180;
#endif
window.ShowAsDropDown(GUIUtility.GUIToScreenRect(buttonRect), new Vector2(popupWidth, 80));
}
void OnEnable()
{
Color borderColor = EditorGUIUtility.isProSkin ? new Color(0.44f, 0.44f, 0.44f, 1f) : new Color(0.51f, 0.51f, 0.51f);
rootVisualElement.style.borderLeftWidth = k_BorderWidth;
rootVisualElement.style.borderTopWidth = k_BorderWidth;
rootVisualElement.style.borderRightWidth = k_BorderWidth;
rootVisualElement.style.borderBottomWidth = k_BorderWidth;
rootVisualElement.style.borderLeftColor = borderColor;
rootVisualElement.style.borderTopColor = borderColor;
rootVisualElement.style.borderRightColor = borderColor;
rootVisualElement.style.borderBottomColor = borderColor;
rootVisualElement.Add(m_FlowDirection = new Toggle(L10n.Tr("Flow Direction")));
m_FlowDirection.style.flexDirection = FlexDirection.RowReverse;
rootVisualElement.Add(m_AllTangents = new Toggle(L10n.Tr("All Tangents")));
m_AllTangents.style.flexDirection = FlexDirection.RowReverse;
rootVisualElement.Add(m_KnotIndices = new Toggle(L10n.Tr("Knot Indices")));
m_KnotIndices.style.flexDirection = FlexDirection.RowReverse;
rootVisualElement.Add(m_SplineMesh = new Toggle(L10n.Tr("Show Mesh")));
m_SplineMesh.style.flexDirection = FlexDirection.RowReverse;
m_FlowDirection.RegisterValueChangedCallback((evt) =>
{
SplineHandleSettings.FlowDirectionEnabled = evt.newValue;
SceneView.RepaintAll();
});
m_AllTangents.RegisterValueChangedCallback((evt) =>
{
SplineHandleSettings.ShowAllTangents = evt.newValue;
SceneView.RepaintAll();
});
m_KnotIndices.RegisterValueChangedCallback((evt) =>
{
SplineHandleSettings.ShowKnotIndices = evt.newValue;
SceneView.RepaintAll();
});
m_SplineMesh.RegisterValueChangedCallback((evt) =>
{
SplineHandleSettings.ShowMesh = evt.newValue;
SceneView.RepaintAll();
});
UpdateValues();
}
void UpdateValues()
{
m_FlowDirection.SetValueWithoutNotify(SplineHandleSettings.FlowDirectionEnabled);
m_AllTangents.SetValueWithoutNotify(SplineHandleSettings.ShowAllTangents);
m_KnotIndices.SetValueWithoutNotify(SplineHandleSettings.ShowKnotIndices);
m_SplineMesh.SetValueWithoutNotify(SplineHandleSettings.ShowMesh);
SceneView.RepaintAll();
}
}
}

View File

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

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using UnityEditor.EditorTools;
using UnityEditor.Overlays;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
[Icon("UnityEditor.InspectorWindow")]
[Overlay(typeof(SceneView), "unity-spline-inspector", "Element Inspector", "SplineInspector")]
sealed class SplineInspectorOverlay : Overlay, ITransientOverlay
{
internal static readonly string SplineChangeUndoMessage = L10n.Tr("Apply Changes to Spline");
public static void ForceUpdate()
{
s_ForceUpdateRequested?.Invoke();
}
static event Action s_ForceUpdateRequested;
static bool s_FirstUpdateSinceDomainReload = true;
static IReadOnlyList<SplineInfo> m_SelectedSplines;
internal static void SetSelectedSplines(IReadOnlyList<SplineInfo> splines)
{
m_SelectedSplines = splines;
if (s_FirstUpdateSinceDomainReload)
{
s_FirstUpdateSinceDomainReload = false;
ForceUpdate();
}
}
public bool visible => ToolManager.activeContextType == typeof(SplineToolContext) && ToolManager.activeToolType != typeof(KnotPlacementTool);
ElementInspector m_ElementInspector;
public override VisualElement CreatePanelContent()
{
VisualElement root = new VisualElement();
root.Add(m_ElementInspector = new ElementInspector());
UpdateInspector();
return root;
}
public override void OnCreated()
{
displayedChanged += OnDisplayedChange;
SplineSelection.changed += UpdateInspector;
s_ForceUpdateRequested += UpdateInspector;
Undo.undoRedoPerformed += OnUndoRedoPerformed;
}
public override void OnWillBeDestroyed()
{
displayedChanged -= OnDisplayedChange;
SplineSelection.changed -= UpdateInspector;
s_ForceUpdateRequested -= UpdateInspector;
Undo.undoRedoPerformed -= OnUndoRedoPerformed;
}
void OnDisplayedChange(bool displayed)
{
UpdateInspector();
}
void UpdateInspector()
{
if (m_SelectedSplines == null)
return;
m_ElementInspector?.UpdateSelection(m_SelectedSplines);
}
void OnUndoRedoPerformed()
{
ForceUpdate();
}
}
}

View File

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

View File

@@ -0,0 +1,135 @@
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
{
sealed class TangentDrawer : ElementDrawer<SelectableTangent>
{
const string k_TangentDrawerStyle = "tangent-drawer";
static readonly List<float> s_LengthBuffer = new List<float>(0);
static readonly SplineGUIUtility.EqualityComparer<float> s_MagnitudeComparer = (a, b) => a.Equals(b);
readonly TangentModePropertyField<SelectableTangent> m_Mode;
readonly BezierTangentPropertyField<SelectableTangent> m_BezierMode;
FloatField m_Magnitude;
Float3PropertyField<SelectableTangent> m_Direction;
public TangentDrawer()
{
AddToClassList(k_TangentDrawerStyle);
Add(m_Mode = new TangentModePropertyField<SelectableTangent>());
m_Mode.changed += () =>
{
m_BezierMode.Update(targets);
EnableElements();
};
Add(m_BezierMode = new BezierTangentPropertyField<SelectableTangent>());
m_BezierMode.changed += () =>
{
m_Mode.Update(targets);
EnableElements();
};
CreateTangentFields();
m_Magnitude.RegisterValueChangedCallback((evt) =>
{
var value = evt.newValue;
if (evt.newValue < 0f)
{
m_Magnitude.SetValueWithoutNotify(0f);
value = 0f;
}
Undo.RecordObject(target.SplineInfo.Object, SplineInspectorOverlay.SplineChangeUndoMessage);
UpdateTangentMagnitude(value);
var tangent = target;
m_Direction.SetValueWithoutNotify(tangent.LocalPosition);
});
}
public override string GetLabelForTargets()
{
if (targets.Count > 1)
return $"<b>({targets.Count}) Tangents</b> selected";
var inOutLabel = target.TangentIndex == 0 ? "In" : "Out";
return $"Tangent <b>{inOutLabel}</b> selected (<b>Knot {target.KnotIndex}</b>, <b>Spline {target.SplineInfo.Index}</b>)";
}
public override void Update()
{
base.Update();
m_Mode.Update(targets);
m_BezierMode.Update(targets);
UpdateMagnitudeField(targets);
m_Direction.Update(targets);
EnableElements();
}
void CreateTangentFields()
{
Add(m_Magnitude = new FloatField(L10n.Tr("Length"), 6));
Add(m_Direction = new Float3PropertyField<SelectableTangent>(L10n.Tr("Direction"),
(tangent) => tangent.LocalDirection,
(tangent, value) => tangent.LocalDirection = value)
{ name = "direction" });
m_Direction.changed += () => { UpdateMagnitudeField(targets); };
}
void UpdateMagnitudeField(IReadOnlyList<SelectableTangent> tangents)
{
s_LengthBuffer.Clear();
for (int i = 0; i < tangents.Count; ++i)
s_LengthBuffer.Add(math.length(tangents[i].LocalPosition));
m_Magnitude.showMixedValue = SplineGUIUtility.HasMultipleValues(s_LengthBuffer, s_MagnitudeComparer);
if (!m_Magnitude.showMixedValue)
m_Magnitude.SetValueWithoutNotify(s_LengthBuffer[0]);
}
void UpdateTangentMagnitude(float value)
{
ElementInspector.ignoreKnotCallbacks = true;
for (int i = 0; i < targets.Count; ++i)
{
var direction = new float3(0, 0, 1);
var tangent = targets[i];
if (math.length(tangent.LocalPosition) > 0)
direction = math.normalize(tangent.LocalPosition);
tangent.LocalPosition = value * direction;
}
ElementInspector.ignoreKnotCallbacks = false;
}
void EnableElements()
{
bool tangentsModifiable = true;
bool tangentsBroken = true;
for (int i = 0; i < targets.Count; ++i)
{
var mode = targets[i].Owner.Mode;
tangentsModifiable &= SplineUtility.AreTangentsModifiable(mode);
tangentsBroken &= mode == TangentMode.Broken;
}
m_Direction.SetEnabled(tangentsModifiable && tangentsBroken);
m_Magnitude.SetEnabled(tangentsModifiable);
}
}
}

View File

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