first commit

This commit is contained in:
lethanhsonvsp
2025-11-17 15:36:52 +07:00
commit 6f2eafa33c
14093 changed files with 1253472 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Unity.Splines.Editor.Tests")]
[assembly: InternalsVisibleTo("Unity.Splines.Editor.Tests")]
[assembly: InternalsVisibleTo("Unity.Splines.Editor.Debug")]

View File

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

View File

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

View File

@@ -0,0 +1,352 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Splines;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace UnityEditor.Splines
{
[CustomEditor(typeof(SplineAnimate))]
[CanEditMultipleObjects]
class SplineAnimateEditor : UnityEditor.Editor
{
List<VisualElement> m_Roots = new ();
List<Slider> m_ProgressSliders = new ();
List<FloatField> m_ElapsedTimeFields = new ();
List<EnumField> m_ObjectForwardFields = new ();
List<EnumField> m_ObjectUpFields = new ();
SerializedProperty m_MethodProperty;
SerializedProperty m_ObjectForwardProperty;
SerializedProperty m_ObjectUpProperty;
SerializedProperty m_StartOffsetProperty;
SerializedObject m_TransformSO;
SplineAnimate m_SplineAnimate;
const string k_UxmlPath = "Packages/com.unity.splines/Editor/Resources/UI/UXML/splineanimate-inspector.uxml";
static VisualTreeAsset s_TreeAsset;
static StyleSheet s_ThemeStyleSheet;
SplineAnimate[] m_Components;
void OnEnable()
{
m_SplineAnimate = target as SplineAnimate;
if (m_SplineAnimate == null)
return;
m_SplineAnimate.Updated += OnSplineAnimateUpdated;
try {
m_MethodProperty = serializedObject.FindProperty("m_Method");
m_ObjectForwardProperty = serializedObject.FindProperty("m_ObjectForwardAxis");
m_ObjectUpProperty = serializedObject.FindProperty("m_ObjectUpAxis");
m_StartOffsetProperty = serializedObject.FindProperty("m_StartOffset");
}
catch (Exception)
{
return;
}
m_TransformSO = new SerializedObject(m_SplineAnimate.transform);
m_Components = targets.Select(x => x as SplineAnimate).Where(y => y != null).ToArray();
foreach (var animate in m_Components)
{
if (animate.Container != null)
animate.RecalculateAnimationParameters();
}
m_Roots.Clear();
m_ObjectForwardFields.Clear();
m_ObjectUpFields.Clear();
m_ProgressSliders.Clear();
m_ElapsedTimeFields.Clear();
EditorApplication.update += OnEditorUpdate;
Spline.Changed += OnSplineChange;
SplineContainer.SplineAdded += OnContainerSplineSetModified;
SplineContainer.SplineRemoved += OnContainerSplineSetModified;
}
void OnDisable()
{
if(m_SplineAnimate != null)
m_SplineAnimate.Updated -= OnSplineAnimateUpdated;
if (!EditorApplication.isPlaying)
{
foreach (var animate in m_Components)
{
if (animate.Container != null)
{
animate.RecalculateAnimationParameters();
animate.Restart(false);
}
}
}
EditorApplication.update -= OnEditorUpdate;
Spline.Changed -= OnSplineChange;
SplineContainer.SplineAdded -= OnContainerSplineSetModified;
SplineContainer.SplineRemoved -= OnContainerSplineSetModified;
}
void OnEditorUpdate()
{
if (!EditorApplication.isPlaying)
{
if (m_SplineAnimate.Container != null && m_SplineAnimate.IsPlaying)
{
m_SplineAnimate.Update();
RefreshProgressFields();
}
}
else if(m_SplineAnimate.IsPlaying)
RefreshProgressFields();
}
void OnSplineChange(Spline spline, int knotIndex, SplineModification modificationType)
{
if (EditorApplication.isPlayingOrWillChangePlaymode)
return;
foreach (var animate in m_Components)
{
if (animate.Container != null && animate.Container.Splines.Contains(spline))
animate.RecalculateAnimationParameters();
}
}
void OnContainerSplineSetModified(SplineContainer container, int spline)
{
if (EditorApplication.isPlayingOrWillChangePlaymode)
return;
foreach (var animate in m_Components)
{
if (animate.Container == container)
animate.RecalculateAnimationParameters();
}
}
public override VisualElement CreateInspectorGUI()
{
var root = new VisualElement();
if (s_TreeAsset == null)
s_TreeAsset = (VisualTreeAsset)AssetDatabase.LoadAssetAtPath(k_UxmlPath, typeof(VisualTreeAsset));
s_TreeAsset.CloneTree(root);
if (s_ThemeStyleSheet == null)
s_ThemeStyleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>($"Packages/com.unity.splines/Editor/Stylesheets/SplineAnimateInspector{(EditorGUIUtility.isProSkin ? "Dark" : "Light")}.uss");
root.styleSheets.Add(s_ThemeStyleSheet);
var methodField = root.Q<PropertyField>("method");
methodField.RegisterValueChangeCallback((_) => { RefreshMethodParamFields((SplineAnimate.Method)m_MethodProperty.enumValueIndex); });
RefreshMethodParamFields((SplineAnimate.Method)m_MethodProperty.enumValueIndex);
var objectForwardField = root.Q<EnumField>("object-forward");
objectForwardField.RegisterValueChangedCallback((evt) => OnObjectAxisFieldChange(evt, m_ObjectForwardProperty, m_ObjectUpProperty));
var objectUpField = root.Q<EnumField>("object-up");
objectUpField.RegisterValueChangedCallback((evt) => OnObjectAxisFieldChange(evt, m_ObjectUpProperty, m_ObjectForwardProperty));
var playButton = root.Q<Button>("play");
playButton.SetEnabled(!EditorApplication.isPlaying);
playButton.clicked += OnPlayClicked;
var pauseButton = root.Q<Button>("pause");
pauseButton.SetEnabled(!EditorApplication.isPlaying);
pauseButton.clicked += OnPauseClicked;
var resetButton = root.Q<Button>("reset");
resetButton.SetEnabled(!EditorApplication.isPlaying);
resetButton.clicked += OnResetClicked;
var progressSlider = root.Q<Slider>("normalized-progress");
progressSlider.SetEnabled(!EditorApplication.isPlaying);
progressSlider.RegisterValueChangedCallback((evt) => OnProgressSliderChange(evt.newValue));
var elapsedTimeField = root.Q<FloatField>("elapsed-time");
elapsedTimeField.SetEnabled(!EditorApplication.isPlaying);
elapsedTimeField.RegisterValueChangedCallback((evt) => OnElapsedTimeFieldChange(evt.newValue));
var startOffsetField = root.Q<PropertyField>("start-offset");
startOffsetField.RegisterValueChangeCallback((evt) =>
{
m_SplineAnimate.StartOffset = m_StartOffsetProperty.floatValue;
if (!EditorApplication.isPlayingOrWillChangePlaymode)
{
m_SplineAnimate.Restart(false);
OnElapsedTimeFieldChange(elapsedTimeField.value);
}
});
m_Roots.Add(root);
m_ProgressSliders.Add(progressSlider);
m_ElapsedTimeFields.Add(elapsedTimeField);
m_ObjectForwardFields.Add(objectForwardField);
m_ObjectUpFields.Add(objectUpField);
return root;
}
void RefreshMethodParamFields(SplineAnimate.Method method)
{
foreach (var root in m_Roots)
{
var durationField = root.Q<PropertyField>("duration");
var maxSpeedField = root.Q<PropertyField>("max-speed");
if (method == (int)SplineAnimate.Method.Time)
{
durationField.style.display = DisplayStyle.Flex;
maxSpeedField.style.display = DisplayStyle.None;
}
else
{
durationField.style.display = DisplayStyle.None;
maxSpeedField.style.display = DisplayStyle.Flex;
}
}
}
void RefreshProgressFields()
{
for (int i = 0; i < m_ProgressSliders.Count && i < m_ElapsedTimeFields.Count; ++i)
{
var progressSlider = m_ProgressSliders[i];
var elapsedTimeField = m_ElapsedTimeFields[i];
if (progressSlider == null || elapsedTimeField == null)
continue;
progressSlider.SetValueWithoutNotify(m_SplineAnimate.GetLoopInterpolation(false));
elapsedTimeField.SetValueWithoutNotify(m_SplineAnimate.ElapsedTime);
}
}
void OnProgressSliderChange(float progress)
{
m_SplineAnimate.Pause();
m_SplineAnimate.NormalizedTime = progress;
RefreshProgressFields();
}
void OnElapsedTimeFieldChange(float elapsedTime)
{
m_SplineAnimate.Pause();
m_SplineAnimate.ElapsedTime = elapsedTime;
RefreshProgressFields();
}
void OnObjectAxisFieldChange(ChangeEvent<Enum> changeEvent, SerializedProperty axisProp, SerializedProperty otherAxisProp)
{
if (changeEvent.newValue == null)
return;
var newValue = (SplineAnimate.AlignAxis)changeEvent.newValue;
var previousValue = (SplineAnimate.AlignAxis)changeEvent.previousValue;
// Swap axes if the picked value matches that of the other axis field
if (newValue == (SplineAnimate.AlignAxis)otherAxisProp.enumValueIndex)
{
otherAxisProp.enumValueIndex = (int)previousValue;
serializedObject.ApplyModifiedProperties();
}
// Prevent the user from configuring object's forward and up as opposite axes
if (((int) newValue) % 3 == otherAxisProp.enumValueIndex % 3)
{
axisProp.enumValueIndex = (int)previousValue;
serializedObject.ApplyModifiedPropertiesWithoutUndo();
}
foreach (var objectForwardField in m_ObjectForwardFields)
objectForwardField.SetValueWithoutNotify((SplineComponent.AlignAxis)m_ObjectForwardProperty.enumValueIndex);
foreach (var objectUpField in m_ObjectUpFields)
objectUpField.SetValueWithoutNotify((SplineComponent.AlignAxis)m_ObjectUpProperty.enumValueIndex);
}
void OnPlayClicked()
{
if (!m_SplineAnimate.IsPlaying)
{
m_SplineAnimate.RecalculateAnimationParameters();
if (m_SplineAnimate.NormalizedTime == 1f)
m_SplineAnimate.Restart(true);
else
m_SplineAnimate.Play();
}
}
void OnPauseClicked()
{
m_SplineAnimate.Pause();
}
void OnResetClicked()
{
m_SplineAnimate.RecalculateAnimationParameters();
m_SplineAnimate.Restart(false);
RefreshProgressFields();
}
void OnSplineAnimateUpdated(Vector3 position, Quaternion rotation)
{
if (m_SplineAnimate == null)
return;
if (!EditorApplication.isPlaying)
{
m_TransformSO.Update();
var localPosition = position;
var localRotation = rotation;
if (m_SplineAnimate.transform.parent != null)
{
localPosition = m_SplineAnimate.transform.parent.worldToLocalMatrix.MultiplyPoint3x4(position);
localRotation = Quaternion.Inverse(m_SplineAnimate.transform.parent.rotation) * localRotation;
}
m_TransformSO.FindProperty("m_LocalPosition").vector3Value = localPosition;
m_TransformSO.FindProperty("m_LocalRotation").quaternionValue = localRotation;
m_TransformSO.ApplyModifiedProperties();
}
}
[DrawGizmo(GizmoType.Selected | GizmoType.Active)]
static void DrawSplineAnimateGizmos(SplineAnimate splineAnimate, GizmoType gizmoType)
{
if (splineAnimate.Container == null)
return;
const float k_OffsetGizmoSize = 0.15f;
splineAnimate.Container.Evaluate(splineAnimate.StartOffsetT, out var offsetPos, out var forward, out var up);
#if UNITY_2022_2_OR_NEWER
using (new Handles.DrawingScope(Handles.elementColor))
#else
using (new Handles.DrawingScope(SplineHandleUtility.knotColor))
#endif
if (Vector3.Magnitude(forward) <= Mathf.Epsilon)
{
if (splineAnimate.StartOffsetT < 1f)
forward = splineAnimate.Container.EvaluateTangent(Mathf.Min(1f, splineAnimate.StartOffsetT + 0.01f));
else
forward = splineAnimate.Container.EvaluateTangent(splineAnimate.StartOffsetT - 0.01f);
}
Handles.ConeHandleCap(-1, offsetPos, Quaternion.LookRotation(Vector3.Normalize(forward), up), k_OffsetGizmoSize * HandleUtility.GetHandleSize(offsetPos), EventType.Repaint);
}
}
}

View File

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

View File

@@ -0,0 +1,42 @@
using System;
using UnityEngine;
using UnityEditor;
class SplineComponentEditor : Editor
{
static GUIStyle s_FoldoutStyle;
internal static readonly string k_Helpbox = L10n.Tr("Instantiated Objects need a SplineContainer target to be created.");
protected bool Foldout(bool foldout, GUIContent content)
{
return Foldout(foldout, content, false);
}
public static bool Foldout(bool foldout, GUIContent content, bool toggleOnLabelClick)
{
if (s_FoldoutStyle == null)
{
s_FoldoutStyle = new GUIStyle(EditorStyles.foldout);
s_FoldoutStyle.fontStyle = FontStyle.Bold;
}
return EditorGUILayout.Foldout(foldout, content, toggleOnLabelClick, s_FoldoutStyle);
}
internal struct LabelWidthScope : IDisposable
{
float previousWidth;
public LabelWidthScope(float width)
{
previousWidth = EditorGUIUtility.labelWidth;
EditorGUIUtility.labelWidth = width;
}
public void Dispose()
{
EditorGUIUtility.labelWidth = previousWidth;
}
}
}

View File

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

View File

@@ -0,0 +1,104 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
// Multi-object selection is not supported
[CustomEditor(typeof(SplineContainer))]
class SplineContainerEditor : UnityEditor.Editor
{
SerializedProperty m_SplineProperty;
SerializedProperty splinesProperty => m_SplineProperty ??= serializedObject.FindProperty("m_Splines");
static GUIStyle s_HelpLabelStyle;
static GUIStyle HelpLabelStyle
{
get
{
if (s_HelpLabelStyle == null)
{
s_HelpLabelStyle = new GUIStyle(EditorStyles.helpBox);
s_HelpLabelStyle.padding = new RectOffset(2, 2, 2, 2);
}
return s_HelpLabelStyle;
}
}
static GUIContent m_HelpLabelContent;
const string k_HelpBoxIconPath = "SplineEditMode-Info";
static GUIContent m_HelpLabelContentIcon;
const string k_ComponentMessage = "Use the Spline Edit Mode in the Scene Tools Overlay to edit this Spline.";
public void OnEnable()
{
m_HelpLabelContent = EditorGUIUtility.TrTextContent(k_ComponentMessage);
m_HelpLabelContentIcon = new GUIContent(PathIcons.GetIcon(k_HelpBoxIconPath));
Undo.undoRedoPerformed += UndoRedoPerformed;
}
public void OnDisable()
{
Undo.undoRedoPerformed -= UndoRedoPerformed;
}
void UndoRedoPerformed()
{
foreach (var t in targets)
{
var container = t as SplineContainer;
if (container != null)
{
container.ClearCaches();
foreach (var spline in container.Splines)
spline.SetDirty(SplineModification.Default);
}
}
}
public override void OnInspectorGUI()
{
serializedObject.Update();
// [SPLB-132] Reverting to custom helpbox as the default helpbox style as a trouble to handle custom icons
// when using a screen with PixelPerPoints different than 1. This is done in trunk by setting the
// Texture2d.pixelsPerPoints which is an internal property than cannot be access from here.
EditorGUILayout.BeginHorizontal(HelpLabelStyle);
EditorGUIUtility.SetIconSize(new Vector2(32f, 32f));
EditorGUILayout.LabelField(m_HelpLabelContentIcon,
GUILayout.Width(34), GUILayout.MinHeight(34), GUILayout.ExpandHeight(true));
EditorGUIUtility.SetIconSize(Vector2.zero);
EditorGUILayout.LabelField(m_HelpLabelContent,
new GUIStyle(EditorStyles.label){wordWrap = HelpLabelStyle.wordWrap, fontSize = HelpLabelStyle.fontSize, padding = new RectOffset(-2, 0, 0, 0)},
GUILayout.ExpandHeight(true));
EditorGUILayout.EndHorizontal();
SplineReorderableList.Get(splinesProperty).DoLayoutList();
serializedObject.ApplyModifiedProperties();
}
bool HasFrameBounds()
{
foreach (var o in targets)
{
var target = (SplineContainer) o;
foreach (var spline in target.Splines)
if (spline.Count > 0)
return true;
}
return false;
}
Bounds OnGetFrameBounds()
{
List<SplineInfo> splines = new List<SplineInfo>();
EditorSplineUtility.GetSplinesFromTargets(targets, splines);
return EditorSplineUtility.GetBounds(splines);
}
}
}

View File

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

View File

@@ -0,0 +1,304 @@
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Splines;
using UnityEngine.Splines.ExtrusionShapes;
namespace UnityEditor.Splines
{
[CustomEditor(typeof(SplineExtrude))]
[CanEditMultipleObjects]
class SplineExtrudeEditor : SplineComponentEditor
{
SerializedProperty m_Container;
SerializedProperty m_RebuildOnSplineChange;
SerializedProperty m_RebuildFrequency;
SerializedProperty m_SegmentsPerUnit;
SerializedProperty m_Capped;
SerializedProperty m_Radius;
SerializedProperty m_Range;
SerializedProperty m_Shape;
SerializedProperty m_UpdateColliders;
SerializedProperty m_FlipNormals;
static readonly GUIContent k_RangeContent = new GUIContent(L10n.Tr("Range"), L10n.Tr("The section of the Spline to extrude."));
static readonly GUIContent k_AdvancedContent = new GUIContent(L10n.Tr("Advanced"), L10n.Tr("Advanced Spline Extrude settings."));
static readonly GUIContent k_PercentageContent = new GUIContent(L10n.Tr("Percentage"), L10n.Tr("The section of the Spline to extrude in percentages."));
static readonly GUIContent k_ShapeContent = new GUIContent(L10n.Tr("Shape Extrude"), L10n.Tr("Shape Extrude settings."));
static readonly GUIContent k_ShapeSettings = EditorGUIUtility.TrTextContent("Settings");
static readonly GUIContent k_GeometryContent = new GUIContent(L10n.Tr("Geometry"), L10n.Tr("Mesh Geometry settings."));
static readonly string k_SourceSplineContainer = L10n.Tr("Source Spline Container");
static readonly string k_CapEnds = L10n.Tr("Cap Ends");
static readonly string k_AutoRefreshGeneration = L10n.Tr("Auto Refresh Generation");
static readonly string k_To = L10n.Tr("to");
static readonly string k_From = L10n.Tr("from");
SplineExtrude[] m_Components;
bool m_AnyMissingMesh;
protected void OnEnable()
{
m_Container = serializedObject.FindProperty("m_Container");
m_RebuildOnSplineChange = serializedObject.FindProperty("m_RebuildOnSplineChange");
m_RebuildFrequency = serializedObject.FindProperty("m_RebuildFrequency");
m_SegmentsPerUnit = serializedObject.FindProperty("m_SegmentsPerUnit");
m_Capped = serializedObject.FindProperty("m_Capped");
m_Radius = serializedObject.FindProperty("m_Radius");
m_Range = serializedObject.FindProperty("m_Range");
m_UpdateColliders = serializedObject.FindProperty("m_UpdateColliders");
m_Shape = serializedObject.FindProperty("m_Shape");
m_FlipNormals = serializedObject.FindProperty("m_FlipNormals");
m_Components = targets.Select(x => x as SplineExtrude).Where(y => y != null).ToArray();
m_AnyMissingMesh = false;
EditorSplineUtility.AfterSplineWasModified += OnSplineModified;
SplineContainer.SplineAdded += OnContainerSplineSetModified;
SplineContainer.SplineRemoved += OnContainerSplineSetModified;
}
void OnDisable()
{
EditorSplineUtility.AfterSplineWasModified -= OnSplineModified;
SplineContainer.SplineAdded -= OnContainerSplineSetModified;
SplineContainer.SplineRemoved -= OnContainerSplineSetModified;
}
void OnSplineModified(Spline spline)
{
if (EditorApplication.isPlayingOrWillChangePlaymode)
return;
foreach (var extrude in m_Components)
{
if (extrude.Container != null && extrude.Splines.Contains(spline))
extrude.Rebuild();
}
}
void OnContainerSplineSetModified(SplineContainer container, int spline)
{
if (EditorApplication.isPlayingOrWillChangePlaymode)
return;
foreach (var extrude in m_Components)
{
if (extrude.Container == container)
extrude.Rebuild();
}
}
void SetShapeType(ShapeType type)
{
foreach (var extrude in m_Components)
{
if (ShapeTypeUtility.GetShapeType(extrude.Shape) == type)
continue;
Undo.RecordObject(extrude, "Set Extrude Shape");
extrude.Shape = ShapeTypeUtility.CreateShape(type);
m_Shape.isExpanded = true;
}
}
bool CanCapEnds()
{
foreach (var extrude in m_Components)
{
if (!extrude.CanCapEnds)
return false;
}
return true;
}
void SetRebuildOnSplineChange(bool value)
{
foreach (var extrude in m_Components)
{
Undo.RecordObject(extrude, "Set Rebuild on Spline Change.");
extrude.RebuildOnSplineChange = value;
}
}
void CreateMeshAssets(SplineExtrude[] components)
{
foreach (var extrude in components)
{
if (!extrude.TryGetComponent<MeshFilter>(out var filter) || filter.sharedMesh == null)
filter.sharedMesh = extrude.CreateMeshAsset();
}
m_AnyMissingMesh = false;
}
void Rebuild()
{
foreach (var extrude in m_Components)
extrude.Rebuild();
}
public override void OnInspectorGUI()
{
serializedObject.Update();
m_AnyMissingMesh = m_Components.Any(x => x.TryGetComponent<MeshFilter>(out var filter) && filter.sharedMesh == null);
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_Container, new GUIContent(k_SourceSplineContainer, m_Container.tooltip));
if (m_Container.objectReferenceValue == null)
EditorGUILayout.HelpBox(k_Helpbox, MessageType.Warning);
// shape section
m_Shape.isExpanded = Foldout(m_Shape.isExpanded, k_ShapeContent, true);
if (m_Shape.isExpanded)
{
EditorGUI.indentLevel++;
EditorGUI.showMixedValue = m_Shape.hasMultipleDifferentValues;
EditorGUI.BeginChangeCheck();
var shapeType = ShapeTypeUtility.GetShapeType(m_Shape.managedReferenceValue);
shapeType = (ShapeType)EditorGUILayout.EnumPopup(L10n.Tr("Type"), shapeType);
if (EditorGUI.EndChangeCheck())
SetShapeType(shapeType);
EditorGUI.showMixedValue = false;
if (m_Shape.hasVisibleChildren)
EditorGUILayout.PropertyField(m_Shape, k_ShapeSettings, true);
EditorGUI.indentLevel--;
}
// https://unityeditordesignsystem.unity.com/patterns/content-organization recommends 8px spacing for
// vertical groups. padding already adds 4 so just nudge that up for a total of 8
EditorGUILayout.Space(4);
// geometry section
m_Radius.isExpanded = Foldout(m_Radius.isExpanded, k_GeometryContent, true);
if (m_Radius.isExpanded)
{
EditorGUI.indentLevel++;
EditorGUILayout.BeginHorizontal();
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_RebuildOnSplineChange, new GUIContent(k_AutoRefreshGeneration, m_RebuildOnSplineChange.tooltip));
if (m_RebuildOnSplineChange.boolValue)
{
EditorGUI.BeginDisabledGroup(!m_RebuildOnSplineChange.boolValue);
using (new LabelWidthScope(80f))
EditorGUILayout.PropertyField(m_RebuildFrequency, new GUIContent() { text = L10n.Tr("Frequency") });
EditorGUI.EndDisabledGroup();
}
else
{
if (GUILayout.Button(new GUIContent(L10n.Tr("Regenerate"))))
Rebuild();
}
if (EditorGUI.EndChangeCheck() && !m_RebuildOnSplineChange.boolValue)
{
// This is needed to set m_RebuildRequested to the appropriate value.
SetRebuildOnSplineChange(m_RebuildOnSplineChange.boolValue);
}
EditorGUILayout.EndHorizontal();
if (m_AnyMissingMesh)
{
GUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel(" ");
if (GUILayout.Button("Create Mesh Asset"))
CreateMeshAssets(m_Components);
GUILayout.EndHorizontal();
}
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_Radius);
if (EditorGUI.EndChangeCheck())
m_Radius.floatValue = Mathf.Clamp(m_Radius.floatValue, .00001f, 1000f);
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_SegmentsPerUnit);
if (EditorGUI.EndChangeCheck())
m_SegmentsPerUnit.floatValue = Mathf.Clamp(m_SegmentsPerUnit.floatValue, .00001f, 4096f);
var canCapEnds = CanCapEnds();
using (new EditorGUI.DisabledScope(!canCapEnds))
{
EditorGUILayout.PropertyField(m_Capped, new GUIContent(k_CapEnds, m_Capped.tooltip));
if (m_Capped.boolValue && !canCapEnds)
m_Capped.boolValue = false;
}
EditorGUILayout.PropertyField(m_FlipNormals);
EditorGUI.indentLevel--;
}
// advanced section
EditorGUILayout.Space(4);
m_Range.isExpanded = Foldout(m_Range.isExpanded, k_AdvancedContent, true);
if (m_Range.isExpanded)
{
EditorGUI.indentLevel++;
EditorGUI.showMixedValue = m_Range.hasMultipleDifferentValues;
var range = m_Range.vector2Value;
EditorGUI.BeginChangeCheck();
EditorGUILayout.MinMaxSlider(k_RangeContent, ref range.x, ref range.y, 0f, 1f);
if (EditorGUI.EndChangeCheck())
m_Range.vector2Value = range;
EditorGUI.indentLevel++;
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel(k_PercentageContent);
EditorGUI.indentLevel--;
EditorGUI.indentLevel--;
EditorGUI.BeginChangeCheck();
var newRange = new Vector2(range.x, range.y);
using (new LabelWidthScope(30f))
newRange.x = EditorGUILayout.FloatField(k_From, range.x * 100f) / 100f;
using (new LabelWidthScope(15f))
newRange.y = EditorGUILayout.FloatField(k_To, range.y * 100f) / 100f;
if (EditorGUI.EndChangeCheck())
{
newRange.x = Mathf.Min(Mathf.Clamp(newRange.x, 0f, 1f), range.y);
newRange.y = Mathf.Max(newRange.x, Mathf.Clamp(newRange.y, 0f, 1f));
m_Range.vector2Value = newRange;
}
EditorGUI.indentLevel++;
EditorGUILayout.EndHorizontal();
EditorGUI.showMixedValue = false;
EditorGUILayout.PropertyField(m_UpdateColliders);
EditorGUI.indentLevel--;
}
serializedObject.ApplyModifiedProperties();
if (EditorGUI.EndChangeCheck())
{
foreach (var extrude in m_Components)
extrude.Rebuild();
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8ae5aa24d97048439e76e9392765c364
timeCreated: 1637689699

View File

@@ -0,0 +1,661 @@
using System.Linq;
using UnityEditor;
using UnityEditor.Splines;
using UnityEngine;
using UnityEngine.Splines;
class SplineInstantiateGizmoDrawer
{
[DrawGizmo(GizmoType.Selected | GizmoType.Active)]
static void DrawSplineInstantiateGizmos(SplineInstantiate scr, GizmoType gizmoType)
{
var instances = scr.instances;
foreach(var instance in instances)
{
var pos = instance.transform.position;
Handles.color = Color.red;
Handles.DrawAAPolyLine(3f,new []{ pos, pos + 0.25f * instance.transform.right });
Handles.color = Color.green;
Handles.DrawAAPolyLine(3f,new []{pos, pos + 0.25f * instance.transform.up});
Handles.color = Color.blue;
Handles.DrawAAPolyLine(3f,new []{pos, pos + 0.25f * instance.transform.forward});
}
}
}
[CustomPropertyDrawer (typeof(SplineInstantiate.InstantiableItem))]
class InstantiableItemDrawer : PropertyDrawer
{
static readonly string k_ProbabilityTooltip = L10n.Tr("Probability for that element to appear.");
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUIUtility.singleLineHeight;
}
public override void OnGUI(Rect rect, SerializedProperty property, GUIContent label)
{
var prefabProperty = property.FindPropertyRelative(nameof(SplineInstantiate.InstantiableItem.Prefab));
var probaProperty = property.FindPropertyRelative(nameof(SplineInstantiate.InstantiableItem.Probability));
var headerLine = ReserveSpace(EditorGUIUtility.singleLineHeight, ref rect);
using(new SplineInstantiateEditor.LabelWidthScope(0f))
EditorGUI.ObjectField(ReserveLineSpace(headerLine.width - 100, ref headerLine), prefabProperty, new GUIContent(""));
ReserveLineSpace(10, ref headerLine);
EditorGUI.LabelField(ReserveLineSpace(15, ref headerLine), new GUIContent("%", k_ProbabilityTooltip));
probaProperty.floatValue = EditorGUI.FloatField(ReserveLineSpace(60, ref headerLine), probaProperty.floatValue);
}
static Rect ReserveSpace(float height, ref Rect total)
{
Rect current = total;
current.height = height;
total.y += height;
return current;
}
static Rect ReserveLineSpace(float width, ref Rect total)
{
Rect current = total;
current.width = width;
total.x += width;
return current;
}
}
[CustomPropertyDrawer (typeof(SplineInstantiate.AlignAxis))]
class ItemAxisDrawer : PropertyDrawer
{
static int s_LastUpAxis;
public override void OnGUI(Rect rect, SerializedProperty property, GUIContent label)
{
var enumValue = property.intValue;
if(property.name == "m_Up")
{
property.intValue = (int)( (SplineInstantiate.AlignAxis)EditorGUI.EnumPopup(rect, label, (SplineInstantiate.AlignAxis)enumValue));
s_LastUpAxis = property.intValue;
}
else
{
property.intValue = (int)((SplineInstantiate.AlignAxis)EditorGUI.EnumPopup(rect, label, (SplineInstantiate.AlignAxis)enumValue,
(item) =>
{
int axisItem = (int)(SplineInstantiate.AlignAxis)item;
return !(axisItem == s_LastUpAxis || axisItem == (s_LastUpAxis + 3) % 6);
}));
}
}
}
[CustomEditor(typeof(SplineInstantiate),false)]
[CanEditMultipleObjects]
class SplineInstantiateEditor : SplineComponentEditor
{
enum SpawnType
{
Exact,
Random
}
SerializedProperty m_SplineContainer;
SerializedProperty m_ItemsToInstantiate;
SerializedProperty m_InstantiateMethod;
SerializedProperty m_Seed;
SerializedProperty m_Space;
SerializedProperty m_UpAxis;
SerializedProperty m_ForwardAxis;
SerializedProperty m_Spacing;
SerializedProperty m_PositionOffset;
SerializedProperty m_RotationOffset;
SerializedProperty m_ScaleOffset;
SerializedProperty m_AutoRefresh;
static readonly string[] k_SpacingTypesLabels = new []
{
L10n.Tr("Count"),
L10n.Tr("Spacing (Spline)"),
L10n.Tr("Spacing (Linear)")
};
//Setup Section
static readonly string k_Setup = L10n.Tr("Instantiated Object Setup");
static readonly string k_ObjectUp = L10n.Tr("Up Axis");
static readonly string k_ObjectUpTooltip = L10n.Tr("Object axis to use as Up Direction when instantiating on the Spline (default is Y).");
static readonly string k_ObjectForward = L10n.Tr("Forward Axis");
static readonly string k_ObjectForwardTooltip = L10n.Tr("Object axis to use as Forward Direction when instantiating on the Spline (default is Z).");
static readonly string k_AlignTo = L10n.Tr("Align To");
static readonly string k_AlignToTooltip = L10n.Tr("Define the space to use to orientate the instantiated object.");
static readonly string k_Instantiation = L10n.Tr("Instantiation");
static readonly string k_Method = L10n.Tr("Instantiate Method");
static readonly string k_MethodTooltip = L10n.Tr("How instances are generated along the spline.");
static readonly string k_Max = L10n.Tr("Max");
static readonly string k_Min = L10n.Tr("Min");
SpawnType m_SpacingType;
//Offsets
static readonly string k_PositionOffset = L10n.Tr("Position Offset");
static readonly string k_PositionOffsetTooltip = L10n.Tr("Whether or not to use a position offset.");
static readonly string k_RotationOffset = L10n.Tr("Rotation Offset");
static readonly string k_RotationOffsetTooltip = L10n.Tr("Whether or not to use a rotation offset.");
static readonly string k_ScaleOffset = L10n.Tr("Scale Offset");
static readonly string k_ScaleOffsetTooltip = L10n.Tr("Whether or not to use a scale offset.");
//Generation
static readonly string k_Generation = L10n.Tr("Generation");
static readonly string k_AutoRefresh = L10n.Tr("Auto Refresh Generation");
static readonly string k_AutoRefreshTooltip = L10n.Tr("Automatically refresh the instances when the spline or the values are changed.");
static readonly string k_Seed = L10n.Tr("Randomization Seed");
static readonly string k_SeedTooltip = L10n.Tr("Value used to initialize the pseudorandom number generator of the instances.");
static readonly string k_Randomize = L10n.Tr("Randomize");
static readonly string k_RandomizeTooltip = L10n.Tr("Compute a new randomization of the instances along the spline.");
static readonly string k_Regenerate = L10n.Tr("Regenerate");
static readonly string k_RegenerateTooltip = L10n.Tr("Regenerate the instances along the spline.");
static readonly string k_Clear = L10n.Tr("Clear");
static readonly string k_ClearTooltip = L10n.Tr("Clear the instances along the spline.");
static readonly string k_Bake = L10n.Tr("Bake Instances");
static readonly string k_BakeTooltip = L10n.Tr("Bake the instances in the SceneView for custom edition and destroy that SplineInstantiate component.");
bool m_PositionFoldout;
bool m_RotationFoldout;
bool m_ScaleFoldout;
enum OffsetType
{
Exact,
Random
};
SplineInstantiate[] m_Components;
SplineInstantiate[] components
{
get
{
//in case of multiple selection where some objects do not have a SplineInstantiate component, m_Components might be null
if (m_Components == null)
m_Components = targets.Select(x => x as SplineInstantiate).Where(y => y != null).ToArray();
return m_Components;
}
}
protected void OnEnable()
{
Spline.Changed += OnSplineChanged;
EditorSplineUtility.AfterSplineWasModified += OnSplineModified;
SplineContainer.SplineAdded += OnContainerSplineSetModified;
SplineContainer.SplineRemoved += OnContainerSplineSetModified;
}
bool Initialize()
{
if (m_Components != null && m_Components.Length > 0)
return true;
m_SplineContainer = serializedObject.FindProperty("m_Container");
m_ItemsToInstantiate = serializedObject.FindProperty("m_ItemsToInstantiate");
m_InstantiateMethod = serializedObject.FindProperty("m_Method");
m_Space = serializedObject.FindProperty("m_Space");
m_UpAxis = serializedObject.FindProperty("m_Up");
m_ForwardAxis = serializedObject.FindProperty("m_Forward");
m_Spacing = serializedObject.FindProperty("m_Spacing");
m_PositionOffset = serializedObject.FindProperty("m_PositionOffset");
m_RotationOffset = serializedObject.FindProperty("m_RotationOffset");
m_ScaleOffset = serializedObject.FindProperty("m_ScaleOffset");
m_Seed = serializedObject.FindProperty("m_Seed");
m_AutoRefresh = serializedObject.FindProperty("m_AutoRefresh");
if (m_Spacing != null)
m_SpacingType = Mathf.Approximately(m_Spacing.vector2Value.x, m_Spacing.vector2Value.y) ? SpawnType.Exact : SpawnType.Random;
else
m_SpacingType = SpawnType.Exact;
m_Components = targets.Select(x => x as SplineInstantiate).Where(y => y != null).ToArray();
return m_Components != null && m_Components.Length > 0;
}
void OnDisable()
{
m_Components = null;
Spline.Changed -= OnSplineChanged;
EditorSplineUtility.AfterSplineWasModified -= OnSplineModified;
SplineContainer.SplineAdded -= OnContainerSplineSetModified;
SplineContainer.SplineRemoved -= OnContainerSplineSetModified;
}
void OnSplineModified(Spline spline)
{
if (EditorApplication.isPlayingOrWillChangePlaymode)
return;
foreach (var instantiate in components)
{
if(instantiate == null)
continue;
if (instantiate.Container != null && instantiate.Container.Splines.Contains(spline))
instantiate.SetSplineDirty(spline);
}
}
void OnSplineChanged(Spline spline, int knotIndex, SplineModification modification)
{
OnSplineModified(spline);
}
void OnContainerSplineSetModified(SplineContainer container, int spline)
{
if (EditorApplication.isPlayingOrWillChangePlaymode)
return;
foreach (var instantiate in components)
{
if (instantiate.Container == container)
instantiate.UpdateInstances();
}
}
public override void OnInspectorGUI()
{
if(!Initialize())
return;
serializedObject.Update();
var dirtyInstances = false;
var updateInstances = false;
EditorGUILayout.PropertyField(m_SplineContainer);
if(m_SplineContainer.objectReferenceValue == null)
EditorGUILayout.HelpBox(k_Helpbox, MessageType.Warning);
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_ItemsToInstantiate);
dirtyInstances = EditorGUI.EndChangeCheck();
DoSetupSection();
dirtyInstances |= DoInstantiateSection();
updateInstances |= DisplayOffsets();
EditorGUILayout.LabelField(k_Generation, EditorStyles.boldLabel);
EditorGUI.indentLevel++;
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_Seed, new GUIContent(k_Seed, k_SeedTooltip));
var newSeed = EditorGUI.EndChangeCheck();
dirtyInstances |= newSeed;
updateInstances |= newSeed;
EditorGUILayout.PropertyField(m_AutoRefresh, new GUIContent(k_AutoRefresh, k_AutoRefreshTooltip));
EditorGUI.indentLevel--;
serializedObject.ApplyModifiedProperties();
EditorGUILayout.Separator();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.Space();
if (GUILayout.Button(new GUIContent(k_Randomize, k_RandomizeTooltip), GUILayout.MaxWidth(100f)))
{
Undo.SetCurrentGroupName("Change SplineInstantiate Seed");
var group = Undo.GetCurrentGroup();
foreach (var splineInstantiate in m_Components)
{
Undo.RecordObject(splineInstantiate, $"Change SplineInstantiate Seed for {splineInstantiate.gameObject.name}");
splineInstantiate.Randomize();
}
Undo.CollapseUndoOperations(group);
updateInstances = true;
}
var isInstancesCountGreaterThanZero = false;
foreach (var splineInstantiate in m_Components)
{
if (splineInstantiate.instances.Count > 0)
{
isInstancesCountGreaterThanZero = true;
break;
}
}
if (GUILayout.Button(new GUIContent(k_Regenerate, k_RegenerateTooltip), GUILayout.MaxWidth(100f)))
updateInstances = true;
GUI.enabled = isInstancesCountGreaterThanZero;
if (GUILayout.Button(new GUIContent(k_Clear, k_ClearTooltip), GUILayout.MaxWidth(100f)))
{
Undo.SetCurrentGroupName("Clear SplineInstantiate");
var group = Undo.GetCurrentGroup();
foreach (var splineInstantiate in m_Components)
{
Undo.RecordObject(splineInstantiate, $"Clear SplineInstantiate for {splineInstantiate.gameObject.name}");
splineInstantiate.Clear();
}
Undo.CollapseUndoOperations(group);
}
if (GUILayout.Button(new GUIContent(k_Bake, k_BakeTooltip), GUILayout.MaxWidth(100f)))
{
foreach (var splineInstantiate in m_Components)
BakeInstances(splineInstantiate);
}
GUI.enabled = true;
EditorGUILayout.Space();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Separator();
foreach (var splineInstantiate in m_Components)
{
if (dirtyInstances)
splineInstantiate.SetDirty();
if (updateInstances)
splineInstantiate.UpdateInstances();
}
if (dirtyInstances || updateInstances)
SceneView.RepaintAll();
}
void DoSetupSection()
{
EditorGUILayout.LabelField(k_Setup, EditorStyles.boldLabel);
GUILayout.Space(5f);
EditorGUI.indentLevel++;
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_UpAxis, new GUIContent(k_ObjectUp, k_ObjectUpTooltip));
EditorGUILayout.PropertyField(m_ForwardAxis, new GUIContent(k_ObjectForward, k_ObjectForwardTooltip));
if(EditorGUI.EndChangeCheck())
{
//Insuring axis integrity
if(m_ForwardAxis.intValue == m_UpAxis.intValue || m_ForwardAxis.intValue == ( m_UpAxis.intValue + 3 ) % 6)
m_ForwardAxis.intValue = ( m_ForwardAxis.intValue + 1 ) % 6;
}
EditorGUILayout.PropertyField(m_Space, new GUIContent(k_AlignTo, k_AlignToTooltip));
EditorGUI.indentLevel--;
}
bool DoInstantiateSection()
{
var dirty = false;
Vector2 spacingV2 = m_Spacing.vector2Value;
EditorGUILayout.LabelField(k_Instantiation, EditorStyles.boldLabel);
EditorGUI.indentLevel++;
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_InstantiateMethod, new GUIContent(k_Method, k_MethodTooltip), EditorStyles.boldFont );
if(EditorGUI.EndChangeCheck())
{
if(m_SpacingType == SpawnType.Random && m_InstantiateMethod.intValue == (int)SplineInstantiate.Method.LinearDistance)
m_Spacing.vector2Value = new Vector2(spacingV2.x, float.NaN);
dirty = true;
}
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel(new GUIContent(k_SpacingTypesLabels[m_InstantiateMethod.intValue]));
EditorGUI.indentLevel--;
GUILayout.Space(2f);
EditorGUI.BeginChangeCheck();
float spacingX = m_Spacing.vector2Value.x;
var isExact = m_SpacingType == SpawnType.Exact;
if(isExact || m_InstantiateMethod.intValue != (int)SplineInstantiate.Method.LinearDistance)
{
using(new LabelWidthScope(30f))
spacingX = (SplineInstantiate.Method)m_InstantiateMethod.intValue == SplineInstantiate.Method.InstanceCount ?
EditorGUILayout.IntField(new GUIContent(isExact ? string.Empty : k_Min), (int)m_Spacing.vector2Value.x, GUILayout.MinWidth(50f)) :
EditorGUILayout.FloatField(new GUIContent(isExact ? L10n.Tr("Dist") : k_Min), m_Spacing.vector2Value.x, GUILayout.MinWidth(50f));
}
if(isExact)
{
spacingV2 = new Vector2(spacingX, spacingX);
}
else if(m_InstantiateMethod.intValue != (int)SplineInstantiate.Method.LinearDistance)
{
using(new LabelWidthScope(30f))
{
var spacingY = (SplineInstantiate.Method)m_InstantiateMethod.intValue == SplineInstantiate.Method.InstanceCount ?
EditorGUILayout.IntField(new GUIContent(k_Max), (int)m_Spacing.vector2Value.y, GUILayout.MinWidth(50f)) :
EditorGUILayout.FloatField(new GUIContent(k_Max), m_Spacing.vector2Value.y, GUILayout.MinWidth(50f));
if(spacingX > m_Spacing.vector2Value.y)
spacingY = spacingX;
else if(spacingY < m_Spacing.vector2Value.x)
spacingX = spacingY;
spacingV2 = new Vector2(spacingX, spacingY);
}
}
if(EditorGUI.EndChangeCheck())
m_Spacing.vector2Value = spacingV2;
EditorGUI.BeginChangeCheck();
if(m_InstantiateMethod.intValue != (int)SplineInstantiate.Method.LinearDistance)
m_SpacingType = (SpawnType)EditorGUILayout.EnumPopup(m_SpacingType, GUILayout.MinWidth(30f));
else
m_SpacingType = (SpawnType)EditorGUILayout.Popup(m_SpacingType == SpawnType.Exact ? 0 : 1,
new []{"Exact", "Auto"}, GUILayout.MinWidth(30f));
if(EditorGUI.EndChangeCheck())
{
if(m_SpacingType == SpawnType.Exact)
m_Spacing.vector2Value = new Vector2(spacingV2.x, spacingV2.x);
else if(m_InstantiateMethod.intValue == (int)SplineInstantiate.Method.LinearDistance)
m_Spacing.vector2Value = new Vector2(spacingV2.x, float.NaN);
dirty = true;
}
EditorGUILayout.EndHorizontal();
return dirty;
}
bool DoOffsetProperties(
SerializedProperty offsetProperty, GUIContent content, bool foldoutValue, out bool newFoldoutValue)
{
bool changed = false;
newFoldoutValue = foldoutValue;
EditorGUILayout.BeginHorizontal();
using(new LabelWidthScope(0f))
{
var setupProperty = offsetProperty.FindPropertyRelative("setup");
var setup = (SplineInstantiate.Vector3Offset.Setup)setupProperty.intValue;
var hasOffset = ( setup & SplineInstantiate.Vector3Offset.Setup.HasOffset ) != 0;
EditorGUI.BeginChangeCheck();
hasOffset = EditorGUILayout.Toggle(hasOffset, GUILayout.MaxWidth(20f));
if(EditorGUI.EndChangeCheck())
{
if(hasOffset)
setup |= SplineInstantiate.Vector3Offset.Setup.HasOffset;
else
setup &= ~SplineInstantiate.Vector3Offset.Setup.HasOffset;
setupProperty.intValue = (int)setup;
changed = true;
}
EditorGUILayout.Space(10f);
using(new EditorGUI.DisabledScope(!hasOffset))
{
newFoldoutValue = Foldout(foldoutValue, content, hasOffset) && hasOffset;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
if(newFoldoutValue)
{
EditorGUILayout.BeginHorizontal();
var hasCustomSpace = ( setup & SplineInstantiate.Vector3Offset.Setup.HasCustomSpace ) != 0;
EditorGUI.BeginChangeCheck();
var space = m_Space.intValue < 1 ? "Spline Element" : m_Space.intValue == 1 ? "Spline Object" : "World";
hasCustomSpace = EditorGUILayout.Toggle(new GUIContent("Override space", L10n.Tr("Override current space (" + space + ")")), hasCustomSpace);
if(EditorGUI.EndChangeCheck())
{
if(hasCustomSpace)
setup |= SplineInstantiate.Vector3Offset.Setup.HasCustomSpace;
else
setup &= ~SplineInstantiate.Vector3Offset.Setup.HasCustomSpace;
setupProperty.intValue = (int)setup;
changed = true;
}
var spaceProperty = offsetProperty.FindPropertyRelative("space");
using(new EditorGUI.DisabledScope(!hasCustomSpace))
{
var type = (SplineInstantiate.OffsetSpace)spaceProperty.intValue;
EditorGUI.BeginChangeCheck();
type = (SplineInstantiate.OffsetSpace)EditorGUILayout.EnumPopup(type);
if(EditorGUI.EndChangeCheck())
{
spaceProperty.intValue = (int)type;
changed = true;
}
}
EditorGUILayout.EndHorizontal();
var minProperty = offsetProperty.FindPropertyRelative("min");
var maxProperty = offsetProperty.FindPropertyRelative("max");
var minPropertyValue = minProperty.vector3Value;
var maxPropertyValue = maxProperty.vector3Value;
float min, max;
SerializedProperty randomProperty;
for(int i = 0; i < 3; i++)
{
string label = i == 0 ? "X" : i == 1 ? "Y" : "Z";
EditorGUILayout.BeginHorizontal();
using(new LabelWidthScope(30f))
EditorGUILayout.LabelField(label);
randomProperty = offsetProperty.FindPropertyRelative("random"+label);
GUILayout.FlexibleSpace();
if(randomProperty.boolValue)
{
EditorGUI.BeginChangeCheck();
using(new LabelWidthScope(30f))
{
min = EditorGUILayout.FloatField("from", minPropertyValue[i], GUILayout.MinWidth(95f), GUILayout.MaxWidth(95f));
max = EditorGUILayout.FloatField(" to", maxPropertyValue[i], GUILayout.MinWidth(95f), GUILayout.MaxWidth(95f));
}
if(EditorGUI.EndChangeCheck())
{
if(min > maxPropertyValue[i])
maxPropertyValue[i] = min;
if(max < minPropertyValue[i])
minPropertyValue[i] = max;
minPropertyValue[i] = min;
maxPropertyValue[i] = max;
minProperty.vector3Value = minPropertyValue;
maxProperty.vector3Value = maxPropertyValue;
changed = true;
}
}
else
{
EditorGUI.BeginChangeCheck();
using(new LabelWidthScope(30f))
min = EditorGUILayout.FloatField("is ", minPropertyValue[i], GUILayout.MinWidth(193f), GUILayout.MaxWidth(193f));
if(EditorGUI.EndChangeCheck())
{
minPropertyValue[i] = min;
if(min > maxPropertyValue[i])
maxPropertyValue[i] = min;
minProperty.vector3Value = minPropertyValue;
maxProperty.vector3Value = maxPropertyValue;
changed = true;
}
}
EditorGUI.BeginChangeCheck();
var isOffsetRandom = randomProperty.boolValue ? OffsetType.Random : OffsetType.Exact;
using(new LabelWidthScope(0f))
isOffsetRandom = (OffsetType)EditorGUILayout.EnumPopup(isOffsetRandom,GUILayout.MinWidth(100f), GUILayout.MaxWidth(200f));
if(EditorGUI.EndChangeCheck())
{
randomProperty.boolValue = isOffsetRandom == OffsetType.Random;
changed = true;
}
EditorGUILayout.EndHorizontal();
}
}
}
}
return changed;
}
bool DisplayOffsets()
{
var updateNeeded = DoOffsetProperties(m_PositionOffset, new GUIContent(k_PositionOffset, k_PositionOffsetTooltip), m_PositionFoldout, out m_PositionFoldout);
updateNeeded |= DoOffsetProperties(m_RotationOffset, new GUIContent(k_RotationOffset, k_RotationOffsetTooltip), m_RotationFoldout, out m_RotationFoldout);
updateNeeded |= DoOffsetProperties(m_ScaleOffset, new GUIContent(k_ScaleOffset, k_ScaleOffsetTooltip), m_ScaleFoldout, out m_ScaleFoldout);
return updateNeeded;
}
/// <summary>
/// Bake the instances into the scene and destroy this SplineInstantiate component.
/// Making changes to the spline after baking will not affect the instances anymore.
/// </summary>
void BakeInstances(SplineInstantiate splineInstantiate)
{
Undo.SetCurrentGroupName("Baking SplineInstantiate instances");
var group = Undo.GetCurrentGroup();
splineInstantiate.UpdateInstances();
for (int i = 0; i < splineInstantiate.instances.Count; ++i)
{
var newInstance = splineInstantiate.instances[i];
newInstance.name = "Instance-" + i;
newInstance.hideFlags = HideFlags.None;
newInstance.transform.SetParent(splineInstantiate.gameObject.transform, true);
Undo.RegisterCreatedObjectUndo(newInstance, "Baking instance");
}
splineInstantiate.instances.Clear();
if(splineInstantiate.InstancesRoot != null)
Undo.DestroyObjectImmediate(splineInstantiate.InstancesRoot);
Undo.DestroyObjectImmediate(splineInstantiate);
Undo.CollapseUndoOperations(group);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,295 @@
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
static class CurveHandles
{
const float k_CurveLineWidth = 4f;
const float k_PreviewCurveOpacity = 0.5f;
static readonly Vector3[] s_CurveDrawingBuffer = new Vector3[SplineCacheUtility.CurveDrawResolution + 1];
static readonly Vector3[] s_FlowTriangleVertices = new Vector3[3];
/// <summary>
/// Creates handles for a BezierCurve.
/// </summary>
/// <param name="controlID">The controlID of the curve to create highlights for.</param>
/// <param name="curve">The <see cref="BezierCurve"/> to create handles for.</param>
public static void Draw(int controlID, BezierCurve curve)
{
if(Event.current.type == EventType.Repaint)
Draw(controlID, curve, false, true);
}
/// <summary>
/// Creates handles for a BezierCurve.
/// </summary>
/// <param name="curve">The <see cref="BezierCurve"/> to create handles for.</param>
/// <param name="activeSpline">Whether the curve is part of the active spline.</param>
internal static void Draw(BezierCurve curve, bool activeSpline)
{
if(Event.current.type == EventType.Repaint)
Draw(0, curve, false, activeSpline);
}
/// <summary>
/// Creates highlights for a BezierCurve to make it easier to select.
/// </summary>
/// <param name="controlID">The controlID of the curve to create highlights for.</param>
/// <param name="curve">The <see cref="BezierCurve"/> to create highlights for.</param>
/// <param name="spline">The <see cref="ISpline"/> (if any) that the curve belongs to.</param>
/// <param name="curveIndex">The curve's index if it belongs to a spline - otherwise -1.</param>
/// <param name="knotA">The knot at the start of the curve.</param>
/// <param name="knotB">The knot at the end of the curve.</param>
/// <param name="activeSpline">Whether the curve is part of the active spline.</param>
internal static void DrawWithHighlight(
int controlID,
ISpline spline,
int curveIndex,
float4x4 localToWorld,
SelectableKnot knotA,
SelectableKnot knotB,
bool activeSpline)
{
var evt = Event.current;
switch(evt.GetTypeForControl(controlID))
{
case EventType.Layout:
case EventType.MouseMove:
if (!SplineHandles.ViewToolActive() && activeSpline)
{
var curve = spline.GetCurve(curveIndex).Transform(localToWorld);
var dist = DistanceToCurve(curve);
HandleUtility.AddControl(controlID, Mathf.Max(0, dist - SplineHandleUtility.pickingDistance));
//Trigger repaint on MouseMove to update highlight visuals from SplineHandles
if (evt.type == EventType.MouseMove || controlID == HandleUtility.nearestControl)
{
SplineHandleUtility.GetNearestPointOnCurve(curve, out _, out var t);
var curveMidT = EditorSplineUtility.GetCurveMiddleInterpolation(curve, spline, curveIndex);
var hoveredKnot = t <= curveMidT ? knotA : knotB;
if (!(SplineHandleUtility.lastHoveredElement is SelectableKnot knot) || !knot.Equals(hoveredKnot))
{
if (GUIUtility.hotControl == 0 && HandleUtility.nearestControl == controlID)
{
SplineHandleUtility.SetLastHoveredElement(hoveredKnot, controlID);
SceneView.RepaintAll();
}
}
}
}
break;
case EventType.MouseDown:
var curveMD = spline.GetCurve(curveIndex).Transform(localToWorld);
if (!SplineHandles.ViewToolActive() && HandleUtility.nearestControl == controlID)
{
//Clicking a knot selects it
if (evt.button != 0)
break;
GUIUtility.hotControl = controlID;
evt.Use();
SplineHandleUtility.GetNearestPointOnCurve(curveMD, out _, out var t);
SplineSelectionUtility.HandleSelection(t <= .5f ? knotA : knotB, false);
}
break;
case EventType.MouseUp:
if (GUIUtility.hotControl == controlID)
{
GUIUtility.hotControl = 0;
evt.Use();
}
break;
}
}
/// <summary>
/// Draws flow on a BezierCurve to indicate the direction.
/// </summary>
/// <param name="curve">The <see cref="BezierCurve"/> to create highlights for.</param>
/// <param name="spline">The <see cref="ISpline"/> (if any) that the curve belongs to.</param>
/// <param name="curveIndex">The curve's index if it belongs to a spline - otherwise -1.</param>
internal static void DrawFlow(BezierCurve curve, ISpline spline, int curveIndex)
{
if(Event.current.type != EventType.Repaint)
return;
var arrow = SplineCacheUtility.GetCurveArrow(spline, curveIndex, curve);
s_FlowTriangleVertices[0] = arrow.positions[0];
s_FlowTriangleVertices[1] = arrow.positions[1];
s_FlowTriangleVertices[2] = arrow.positions[2];
using (new Handles.DrawingScope(SplineHandleUtility.lineColor, arrow.trs))
{
using (new ZTestScope(CompareFunction.Less))
Handles.DrawAAConvexPolygon(s_FlowTriangleVertices);
}
using (new Handles.DrawingScope(SplineHandleUtility.lineBehindColor, arrow.trs))
{
using (new ZTestScope(CompareFunction.Greater))
Handles.DrawAAConvexPolygon(s_FlowTriangleVertices);
}
}
static void Draw(int controlID, BezierCurve curve, bool preview, bool activeSpline)
{
var evt = Event.current;
switch (evt.type)
{
case EventType.Layout:
case EventType.MouseMove:
if (!SplineHandles.ViewToolActive() && activeSpline)
{
var dist = DistanceToCurve(curve);
HandleUtility.AddControl(controlID, Mathf.Max(0, dist - SplineHandleUtility.pickingDistance));
}
break;
case EventType.Repaint:
var prevColor = Handles.color;
FillCurveDrawingBuffer(curve);
var color = SplineHandleUtility.lineColor;
if (preview)
color.a *= k_PreviewCurveOpacity;
Handles.color = color;
using (new ZTestScope(CompareFunction.Less))
Handles.DrawAAPolyLine(SplineHandleUtility.denseLineAATex, k_CurveLineWidth, s_CurveDrawingBuffer);
color = SplineHandleUtility.lineBehindColor;
if (preview)
color.a *= k_PreviewCurveOpacity;
Handles.color = color;
using (new ZTestScope(CompareFunction.Greater))
Handles.DrawAAPolyLine(SplineHandleUtility.denseLineAATex, k_CurveLineWidth, s_CurveDrawingBuffer);
Handles.color = prevColor;
break;
}
}
static void FillCurveDrawingBuffer(BezierCurve curve)
{
SplineCacheUtility.GetCurvePositions(curve, s_CurveDrawingBuffer);
}
internal static float DistanceToCurve(BezierCurve curve)
{
FillCurveDrawingBuffer(curve);
return DistanceToCurve();
}
static float DistanceToCurve()
{
float dist = float.MaxValue;
for (var i = 0; i < s_CurveDrawingBuffer.Length - 1; ++i)
{
var a = s_CurveDrawingBuffer[i];
var b = s_CurveDrawingBuffer[i + 1];
dist = Mathf.Min(HandleUtility.DistanceToLine(a, b), dist);
}
return dist;
}
internal static void DoCurveHighlightCap(SelectableKnot knot)
{
if(Event.current.type != EventType.Repaint)
return;
if(knot.IsValid())
{
var spline = knot.SplineInfo.Spline;
var localToWorld = knot.SplineInfo.LocalToWorld;
if(knot.KnotIndex > 0 || spline.Closed)
{
var curve = spline.GetCurve(spline.PreviousIndex(knot.KnotIndex)).Transform(localToWorld);
var curveMiddleT = EditorSplineUtility.GetCurveMiddleInterpolation(curve, spline, spline.PreviousIndex(knot.KnotIndex));
DrawCurveHighlight(curve, 1f, curveMiddleT);
}
if(knot.KnotIndex < spline.Count - 1 || spline.Closed)
{
var curve = spline.GetCurve(knot.KnotIndex).Transform(localToWorld);
var curveMiddleT = EditorSplineUtility.GetCurveMiddleInterpolation(curve, spline, knot.KnotIndex);
DrawCurveHighlight(curve, 0f, curveMiddleT);
}
}
}
static void DrawCurveHighlight(BezierCurve curve, float startT, float endT)
{
FillCurveDrawingBuffer(curve);
var growing = startT <= endT;
var color = Handles.color;
color.a = growing ? 1f : 0f;
using (new ZTestScope(CompareFunction.Less))
using (new Handles.DrawingScope(color))
DrawAAPolyLineForCurveHighlight(color, startT, endT, 1f, growing);
using (new ZTestScope(CompareFunction.Greater))
using (new Handles.DrawingScope(color))
DrawAAPolyLineForCurveHighlight(color, startT, endT, 0.3f, growing);
}
static void DrawAAPolyLineForCurveHighlight(Color color, float startT, float endT, float colorAlpha, bool growing)
{
for (int i = 1; i <= SplineCacheUtility.CurveDrawResolution; ++i)
{
Handles.DrawAAPolyLine(SplineHandleUtility.denseLineAATex, k_CurveLineWidth, new[] { s_CurveDrawingBuffer[i - 1], s_CurveDrawingBuffer[i] });
var current = ((float)i / (float)SplineCacheUtility.CurveDrawResolution);
if (growing)
{
if (current > endT)
color.a = 0f;
else if (current > startT)
color.a = (1f - (current - startT) / (endT - startT)) * colorAlpha;
}
else
{
if (current < endT)
color.a = 0f;
else if (current > endT && current < startT)
color.a = (current - endT) / (startT - endT) * colorAlpha;
}
Handles.color = color;
}
}
/// <summary>
/// Creates the set of control points that make up a curve.
/// </summary>
/// <param name="curve">The <see cref="BezierCurve"/> to create control points for.</param>
public static void DrawControlNet(BezierCurve curve)
{
Handles.color = Color.green;
Handles.DotHandleCap(-1, curve.P0, Quaternion.identity, HandleUtility.GetHandleSize(curve.P0) * .04f, Event.current.type);
Handles.color = Color.red;
Handles.DotHandleCap(-1, curve.P1, Quaternion.identity, HandleUtility.GetHandleSize(curve.P1) * .04f, Event.current.type);
Handles.color = Color.yellow;
Handles.DotHandleCap(-1, curve.P2, Quaternion.identity, HandleUtility.GetHandleSize(curve.P2) * .04f, Event.current.type);
Handles.color = Color.blue;
Handles.DotHandleCap(-1, curve.P3, Quaternion.identity, HandleUtility.GetHandleSize(curve.P3) * .04f, Event.current.type);
Handles.color = Color.gray;
Handles.DrawDottedLine(curve.P0, curve.P1, 2f);
Handles.DrawDottedLine(curve.P1, curve.P2, 2f);
Handles.DrawDottedLine(curve.P2, curve.P3, 2f);
}
}
}

View File

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

View File

@@ -0,0 +1,201 @@
using Unity.Mathematics;
using UnityEditor.SettingsManagement;
using UnityEngine;
using UnityEngine.Rendering;
namespace UnityEditor.Splines
{
static class DirectManipulation
{
[UserSetting("Tweak Mode", "Plane Color")]
static readonly Pref<Color> s_GuidePlaneColor = new Pref<Color>("Handles.DirectManipulation.PlaneColor", new Color(1f, 1f, 1f, 5f/255f));
[UserSetting("Tweak Mode", "Snap to Guide Enabled")]
static readonly Pref<bool> s_SnapToGuide = new Pref<bool>("Handles.DirectManipulation.SnapToGuide", true);
[UserSetting("Tweak Mode", "Snap to Guide Distance")]
static readonly Pref<float> s_SnapToGuideDistance = new Pref<float>("Handles.DirectManipulation.SnapToGuideDistance", 7f);
static readonly Vector3[] s_VertexBuffer = new Vector3[4];
public static bool IsDragging => s_IsDragging;
static readonly Vector3 k_GuidePlaneZTestOffset = new Vector3(0.001f, 0.001f, 0.001f);
static Vector3 s_InitialPosition;
static Quaternion s_InitialRotation;
static Vector2 s_InitialMousePosition;
static bool s_IsDragging;
#if UNITY_2022_2_OR_NEWER
static bool IncrementalSnapActive => EditorSnapSettings.incrementalSnapActive;
#else
static bool IncrementalSnapActive => false;
#endif
const float k_HandleColorAlphaFactor = 0.3f;
static bool ShouldMoveOnNormal(int controlId) => GUIUtility.hotControl == controlId && Event.current.alt;
public static void BeginDrag(Vector3 position, Quaternion rotation)
{
s_InitialPosition = position;
s_InitialRotation = rotation;
s_InitialMousePosition = Event.current.mousePosition;
}
public static Vector3 UpdateDrag(int controlId)
{
var position = ShouldMoveOnNormal(controlId)
? MoveOnNormal(Event.current.mousePosition, s_InitialPosition, s_InitialRotation)
: MoveOnPlane(Event.current.mousePosition, s_InitialPosition, s_InitialRotation, IncrementalSnapActive);
s_IsDragging = true;
return position;
}
public static void EndDrag()
{
s_IsDragging = false;
}
public static void DrawHandles(int controlId, Vector3 position)
{
if (GUIUtility.hotControl != controlId || !s_IsDragging)
return;
EditorGUIUtility.AddCursorRect(new Rect(0, 0, 100000, 10000), MouseCursor.MoveArrow);
if (ShouldMoveOnNormal(controlId))
{
var yDir = s_InitialRotation * Vector3.up;
DrawGuideAxis(s_InitialPosition, yDir, Handles.yAxisColor);
}
else
{
var zDir = s_InitialRotation * Vector3.forward;
var xDir = s_InitialRotation * Vector3.right;
DrawGuidePlane(s_InitialPosition, xDir, zDir, position);
DrawGuideDottedLine(s_InitialPosition, zDir, position);
DrawGuideDottedLine(s_InitialPosition, xDir, position);
DrawGuideAxis(s_InitialPosition, zDir, Handles.zAxisColor);
DrawGuideAxis(s_InitialPosition, xDir, Handles.xAxisColor);
}
}
static (Vector3 projection, float distance) GetSnapToGuideData(Vector3 current, Vector3 origin, Vector3 axis)
{
var projection = Vector3.Project(current - origin, axis);
var screenPos = HandleUtility.WorldToGUIPoint(origin + projection);
var distance = Vector2.Distance(screenPos, Event.current.mousePosition);
return (projection, distance);
}
static Vector3 MoveOnPlane(Vector2 mousePosition, Vector3 origin, Quaternion rotation, bool snapping)
{
var ray = HandleUtility.GUIPointToWorldRay(mousePosition);
var manipPlane = new Plane(rotation * Vector3.up, origin);
var position = manipPlane.Raycast(ray, out float distance)
? ray.origin + ray.direction * distance
: origin;
var dir = position - origin;
var forward = GetSnapToGuideData(position, origin, rotation * Vector3.forward);
var right = GetSnapToGuideData(position, origin, rotation * Vector3.right);
if (!snapping && s_SnapToGuide)
{
if (forward.distance < s_SnapToGuideDistance || right.distance < s_SnapToGuideDistance)
{
var snapToForward = forward.distance < right.distance;
var axis = (snapToForward ? forward : right).projection;
return origin + axis;
}
}
if(Mathf.Approximately(dir.magnitude, 0f))
dir = Vector3.forward;
var translation = Handles.SnapValue(Quaternion.Inverse(rotation) * dir, new Vector3(EditorSnapSettings.move.x, 0, EditorSnapSettings.move.z));
return origin + rotation * translation;
}
static Vector3 MoveOnNormal(Vector2 mousePosition, Vector3 origin, Quaternion rotation)
{
var upAxis = rotation * Vector3.up;
var translation = upAxis * Handles.SnapValue(HandleUtility.CalcLineTranslation(s_InitialMousePosition, mousePosition, origin, upAxis), EditorSnapSettings.move.y);
return origin + translation;
}
static void DrawGuideAxis(Vector3 origin, Vector3 axis, Color color)
{
var start = origin - axis.normalized * 10000f;
var end = origin + axis.normalized * 10000f;
using (new ZTestScope(CompareFunction.Less))
using (new Handles.DrawingScope(color))
{
Handles.DrawLine(origin, start, 0f);
Handles.DrawLine(origin, end, 0f);
}
color = new Color(color.r, color.g, color.b, color.a * k_HandleColorAlphaFactor);
using (new ZTestScope(CompareFunction.Greater))
using (new Handles.DrawingScope(color))
{
Handles.DrawLine(origin, start, 0f);
Handles.DrawLine(origin, end, 0f);
}
}
static void DrawGuidePlane(Vector3 origin, Vector3 axisX, Vector3 axisZ, Vector3 position)
{
var xAxisProjection = Vector3.Project(position - origin, axisX);
var zAxisProjection = Vector3.Project(position - origin, axisZ);
var cross = math.cross(xAxisProjection, zAxisProjection);
var normal = math.normalizesafe(cross);
var scaledOffset = k_GuidePlaneZTestOffset * HandleUtility.GetHandleSize(origin);
var calculatedOffset = new Vector3(scaledOffset.x * normal.x, scaledOffset.y * normal.y, scaledOffset.z * normal.z);
position += calculatedOffset;
origin += calculatedOffset;
s_VertexBuffer[0] = origin;
s_VertexBuffer[1] = origin + Vector3.Project(position - origin, axisX);
s_VertexBuffer[2] = position;
s_VertexBuffer[3] = origin + Vector3.Project(position - origin, axisZ);
DrawGuidePlane(Matrix4x4.identity);
}
static void DrawGuidePlane(Matrix4x4 matrix)
{
var color = s_GuidePlaneColor.value;
using (new ZTestScope(CompareFunction.Less))
using (new Handles.DrawingScope(matrix))
Handles.DrawSolidRectangleWithOutline(s_VertexBuffer, color, Color.clear);
color = new Color(s_GuidePlaneColor.value.r, s_GuidePlaneColor.value.g, s_GuidePlaneColor.value.b,
s_GuidePlaneColor.value.a * k_HandleColorAlphaFactor);
using (new ZTestScope(CompareFunction.Greater))
using (new Handles.DrawingScope(matrix))
Handles.DrawSolidRectangleWithOutline(s_VertexBuffer, color, Color.clear);
}
static void DrawGuideDottedLine(Vector3 origin, Vector3 axis, Vector3 position)
{
using (new ZTestScope(CompareFunction.Less))
Handles.DrawDottedLine(origin + Vector3.Project(position - origin, axis), position, 3f);
var color = new Color(Handles.color.r, Handles.color.g, Handles.color.b, Handles.color.a * k_HandleColorAlphaFactor);
using (new ZTestScope(CompareFunction.Greater))
using (new Handles.DrawingScope(color))
Handles.DrawDottedLine(origin + Vector3.Project(position - origin, axis), position, 3f);
}
}
}

View File

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

View File

@@ -0,0 +1,322 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
namespace UnityEditor.Splines
{
static class KnotHandles
{
const float k_ColorAlphaFactor = 0.3f;
const float k_KnotRotDiscRadius = 0.18f;
const float k_KnotRotDiscWidthDefault = 1.5f;
const float k_KnotRotDiscWidthHover = 3f;
const float k_KnotHandleWidth = 2f;
static readonly List<SelectableKnot> k_KnotBuffer = new List<SelectableKnot>();
static readonly Vector3[] k_HandlePoints = new Vector3[11];
static List<(SelectableKnot knot, bool selected, bool hovered, Color knotColor, Color discColor, bool linkedKnot)> s_Knots = new ();
internal static void Do(int controlId, SelectableKnot knot, bool selected = false, bool hovered = false)
{
if (Event.current.GetTypeForControl(controlId) != EventType.Repaint)
return;
//Hovered might not be available if a TRS tool is in use
hovered &= SplineHandleUtility.IsHoverAvailableForSplineElement();
var knotColor = SplineHandleUtility.elementColor;
var rotationDiscColor = SplineHandleUtility.elementPreselectionColor;
if (hovered)
knotColor = SplineHandleUtility.elementPreselectionColor;
else if (selected)
knotColor = SplineHandleUtility.elementSelectionColor;
Draw(knot.Position, knot.Rotation, knotColor, selected, hovered, rotationDiscColor, k_KnotRotDiscWidthHover);
DrawKnotIndices(knot);
}
internal static void Draw(int controlId, SelectableKnot knot)
{
if (Event.current.GetTypeForControl(controlId) != EventType.Repaint)
return;
if(!knot.IsValid())
return;
var selected = SplineSelection.Contains(knot);
var knotHovered = SplineHandleUtility.IsElementHovered(controlId);
//Retrieving linked knots
EditorSplineUtility.GetKnotLinks(knot, k_KnotBuffer);
var drawLinkedKnotHandle = k_KnotBuffer.Count != 1;
var mainKnot = knot;
SelectableKnot lastHovered = new SelectableKnot();
// Retrieving the last hovered element
// SplineHandleUtility.lastHoveredElement is pointing either to:
// - the hovered Knot and the ID is pointing to the controlID of that knot in that case
// - if a curve is hovered, the element is the knot closest to the hovered part of the curve (start or end knot depending)
// and the controlID is the one of the curve
var lastHoveredElementIsKnot = SplineHandleUtility.lastHoveredElement is SelectableKnot;
if (lastHoveredElementIsKnot)
lastHovered = (SelectableKnot)SplineHandleUtility.lastHoveredElement;
var isCurveId = SplineHandles.IsCurveId(SplineHandleUtility.lastHoveredElementId);
var curveIsHovered = lastHoveredElementIsKnot &&
k_KnotBuffer.Contains(lastHovered) &&
isCurveId;
var hovered = knotHovered || (curveIsHovered && knot.Equals(lastHovered));
if (drawLinkedKnotHandle)
{
if (curveIsHovered)
{
drawLinkedKnotHandle = false;
if (!knot.Equals(lastHovered))
{
if (!SplineSelection.Contains(knot))
return;
hovered = false;
mainKnot = lastHovered;
}
}
else
{
foreach (var linkedKnot in k_KnotBuffer)
{
if (!hovered)
{
var kSelected = SplineSelection.Contains(linkedKnot);
// If the current knot in not selected but other linked knots are, skip rendering
if (!selected && kSelected)
return;
// If current knot is selected but not k, don't consider k as a potential knot
if (selected && !kSelected)
{
drawLinkedKnotHandle = false;
continue;
}
}
//Main knot is the older one, the one on the spline of lowest range and the knot of lowest index
if ((!SplineSelection.HasActiveSplineSelection() || SplineSelection.Contains(linkedKnot.SplineInfo)) &&
(linkedKnot.SplineInfo.Index < mainKnot.SplineInfo.Index ||
linkedKnot.SplineInfo.Index == mainKnot.SplineInfo.Index && linkedKnot.KnotIndex < mainKnot.KnotIndex))
mainKnot = linkedKnot;
}
}
}
//Hovered might not be available if a TRS tool is in use
hovered &= SplineHandleUtility.IsHoverAvailableForSplineElement();
var knotColor = SplineHandleUtility.elementColor;
var highlightColor = SplineHandleUtility.elementPreselectionColor;
var rotationDiscColor = SplineHandleUtility.elementPreselectionColor;
if (hovered)
{
knotColor = SplineHandleUtility.elementPreselectionColor;
highlightColor = SplineHandleUtility.elementPreselectionColor;
}
else if (selected)
{
knotColor = SplineHandleUtility.elementSelectionColor;
highlightColor = SplineHandleUtility.elementSelectionColor;
}
if (SplineHandleUtility.canDrawOnCurves && (hovered || selected))
{
using (new Handles.DrawingScope(highlightColor))
CurveHandles.DoCurveHighlightCap(knot);
}
if (knot.Equals(mainKnot))
{
s_Knots.Add((knot, selected, hovered, knotColor, rotationDiscColor, drawLinkedKnotHandle));
DrawKnotIndices(knot);
}
}
internal static void ClearVisibleKnots()
{
if (Event.current.type != EventType.Repaint)
return;
s_Knots.Clear();
}
internal static void DrawVisibleKnots()
{
if (Event.current.type != EventType.Repaint)
return;
foreach (var knotInfo in s_Knots)
Draw(knotInfo.knot.Position, knotInfo.knot.Rotation, knotInfo.knotColor, knotInfo.selected, knotInfo.hovered, knotInfo.discColor, k_KnotRotDiscWidthHover, knotInfo.linkedKnot);
}
static void DrawKnotIndices(SelectableKnot knot)
{
if (!SplineHandleSettings.ShowKnotIndices)
return;
var hasLinkedKnots = !(k_KnotBuffer.Count == 1 && k_KnotBuffer.Contains(knot));
if (k_KnotBuffer != null && k_KnotBuffer.Count > 0 && hasLinkedKnots)
{
var stringBuilder = new System.Text.StringBuilder("[");
for (var i = 0; i < k_KnotBuffer.Count; i++)
{
stringBuilder.Append($"({k_KnotBuffer[i].SplineInfo.Index},{k_KnotBuffer[i].KnotIndex})");
if (i != k_KnotBuffer.Count - 1)
stringBuilder.Append(", ");
}
stringBuilder.Append("]");
Handles.Label(knot.Position, stringBuilder.ToString());
}
else
{
Handles.Label(knot.Position, $"[{knot.KnotIndex}]");
}
}
internal static void Draw(SelectableKnot knot, Color knotColor, bool selected, bool hovered)
{
EditorSplineUtility.GetKnotLinks(knot, k_KnotBuffer);
var mainKnot = knot;
if(k_KnotBuffer.Count != 1)
{
foreach(var k in k_KnotBuffer)
{
//Main knot is the older one, the one on the spline of lowest range and the knot of lowest index
if(k.SplineInfo.Index < mainKnot.SplineInfo.Index ||
k.SplineInfo.Index == mainKnot.SplineInfo.Index && k.KnotIndex < mainKnot.KnotIndex)
mainKnot = k;
}
}
if(!mainKnot.Equals(knot))
return;
Draw(knot.Position, knot.Rotation, knotColor, selected, hovered, knotColor, k_KnotRotDiscWidthDefault, k_KnotBuffer.Count != 1);
}
internal static void Draw(Vector3 position, Quaternion rotation, Color knotColor, bool selected, bool hovered)
{
Draw(position, rotation, knotColor, selected, hovered, knotColor, k_KnotRotDiscWidthDefault);
}
static void UpdateHandlePoints(float size)
{
var startIndex = 5;
k_HandlePoints[startIndex] = Vector3.forward * k_KnotRotDiscRadius * size;
var r = Vector3.right * SplineHandleUtility.knotDiscRadiusFactorDefault * size;
//The first and last element should be in the middle of the points list to get a better visual
for(int i = 0; i < 9; i++)
{
var index = ( i + startIndex + 1 ) % k_HandlePoints.Length;
var pos = Quaternion.Euler(0, ( 1f - i / 8f ) * 180f, 0) * r;
k_HandlePoints[index] = pos;
if(index == k_HandlePoints.Length - 1)
{
startIndex += 1;
k_HandlePoints[0] = pos;
}
}
}
internal static void DrawInformativeKnot(SelectableKnot knot, float sizeFactor = 0.5f)
{
if (Event.current.type != EventType.Repaint)
return;
EditorSplineUtility.GetKnotLinks(knot, k_KnotBuffer);
var drawLinkedKnotHandle = k_KnotBuffer.Count != 1;
if(drawLinkedKnotHandle)
{
foreach(var k in k_KnotBuffer)
{
//If the current knot in not selected but other linked knots are, skip rendering
if(SplineSelection.Contains(k))
return;
}
}
DrawInformativeKnotVisual(knot, SplineHandleUtility.lineColor, sizeFactor);
}
static void DrawInformativeKnotVisual(SelectableKnot knot, Color knotColor, float sizeFactor = 0.5f)
{
var position = knot.Position;
var size = HandleUtility.GetHandleSize(position);
using(new Handles.DrawingScope(knotColor, Matrix4x4.TRS(position, knot.Rotation, Vector3.one)))
{
Handles.DrawSolidDisc(Vector3.zero, Vector3.up, size * SplineHandleUtility.knotDiscRadiusFactorSelected * sizeFactor);
}
}
static void Draw(Vector3 position, Quaternion rotation, Color knotColor, bool selected, bool hovered, Color discColor, float rotationDiscWidth, bool linkedKnots = false)
{
var size = HandleUtility.GetHandleSize(position);
using (new ZTestScope(CompareFunction.Less))
{
using (new Handles.DrawingScope(knotColor, Matrix4x4.TRS(position, rotation, Vector3.one)))
DrawKnotShape(size, selected, linkedKnots);
if (hovered)
{
using (new Handles.DrawingScope(discColor, Matrix4x4.TRS(position, rotation, Vector3.one)))
SplineHandleUtility.DrawAAWireDisc(Vector3.zero, Vector3.up, k_KnotRotDiscRadius * size, rotationDiscWidth);
}
}
using (new ZTestScope(CompareFunction.Greater))
{
var newKnotColor = new Color(knotColor.r, knotColor.g, knotColor.b, knotColor.a * k_ColorAlphaFactor);
using (new Handles.DrawingScope(newKnotColor, Matrix4x4.TRS(position, rotation, Vector3.one)))
DrawKnotShape(size, selected, linkedKnots);
if (hovered)
{
var newDiscColor = new Color(discColor.r, discColor.g,
discColor.b, discColor.a * k_ColorAlphaFactor);
using (new Handles.DrawingScope(newDiscColor, Matrix4x4.TRS(position, rotation, Vector3.one)))
SplineHandleUtility.DrawAAWireDisc(Vector3.zero, Vector3.up, k_KnotRotDiscRadius * size, rotationDiscWidth);
}
}
}
static void DrawKnotShape(float size, bool selected, bool linkedKnots)
{
if (!linkedKnots)
{
UpdateHandlePoints(size);
Handles.DrawAAPolyLine(SplineHandleUtility.denseLineAATex, k_KnotHandleWidth, k_HandlePoints);
if (selected)
Handles.DrawAAConvexPolygon(k_HandlePoints);
}
else
{
// Knot disc
if (selected)
{
var radius = selected ? SplineHandleUtility.knotDiscRadiusFactorSelected : SplineHandleUtility.knotDiscRadiusFactorHover;
Handles.DrawSolidDisc(Vector3.zero, Vector3.up, radius * size);
}
else
Handles.DrawWireDisc(Vector3.zero, Vector3.up, SplineHandleUtility.knotDiscRadiusFactorDefault * size, SplineHandleUtility.handleWidth * SplineHandleUtility.aliasedLineSizeMultiplier);
}
Handles.DrawAAPolyLine(Vector3.zero, Vector3.up * 2f * SplineHandleUtility.sizeFactor * size);
}
}
}

View File

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

View File

@@ -0,0 +1,228 @@
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
class SplineElementRectSelector
{
enum Mode
{
None,
Replace,
Add,
Subtract
}
static class Styles
{
public static readonly GUIStyle selectionRect = GUI.skin.FindStyle("selectionRect");
}
Rect m_Rect;
Vector2 m_StartPos;
Mode m_Mode;
Mode m_InitialMode;
static readonly HashSet<ISelectableElement> s_SplineElementsCompareSet = new HashSet<ISelectableElement>();
static readonly List<ISelectableElement> s_SplineElementsBuffer = new List<ISelectableElement>();
static readonly HashSet<ISelectableElement> s_PreRectSelectionElements = new HashSet<ISelectableElement>();
public void OnGUI(IReadOnlyList<SplineInfo> splines)
{
int id = GUIUtility.GetControlID(FocusType.Passive);
Event evt = Event.current;
switch (evt.GetTypeForControl(id))
{
case EventType.Layout:
case EventType.MouseMove:
HandleUtility.AddDefaultControl(id);
if (m_Mode != Mode.None)
{
// If we've started rect select in Add or Subtract modes, then if we were in a Replace
// mode just before (i.e. the shift or action has been released temporarily),
// we need to bring back the pre rect selection elements into current selection.
if (m_InitialMode != Mode.Replace && RefreshSelectionMode())
{
SplineSelection.Clear();
s_SplineElementsCompareSet.Clear();
if (m_Mode != Mode.Replace)
{
foreach (var element in s_PreRectSelectionElements)
SplineSelection.Add(element);
}
m_Rect = GetRectFromPoints(m_StartPos, evt.mousePosition);
UpdateSelection(m_Rect, splines);
}
}
break;
case EventType.Repaint:
if (GUIUtility.hotControl == id && m_Rect.size != Vector2.zero)
{
Handles.BeginGUI();
Styles.selectionRect.Draw(m_Rect, GUIContent.none, false, false, false, false);
Handles.EndGUI();
}
break;
case EventType.MouseDown:
if (SplineHandles.ViewToolActive())
return;
if (HandleUtility.nearestControl == id && evt.button == 0)
{
m_StartPos = evt.mousePosition;
m_Rect = new Rect(Vector3.zero, Vector2.zero);
BeginSelection(splines);
GUIUtility.hotControl = id;
evt.Use();
}
break;
case EventType.MouseDrag:
if (GUIUtility.hotControl == id)
{
m_Rect = GetRectFromPoints(m_StartPos, evt.mousePosition);
evt.Use();
UpdateSelection(m_Rect, splines);
}
break;
case EventType.MouseUp:
if (GUIUtility.hotControl == id)
{
GUIUtility.hotControl = 0;
evt.Use();
EndSelection(m_Rect, splines);
}
break;
}
}
void BeginSelection(IReadOnlyList<SplineInfo> splines)
{
RefreshSelectionMode();
m_InitialMode = m_Mode;
s_SplineElementsCompareSet.Clear();
s_SplineElementsBuffer.Clear();
if (m_Mode == Mode.Replace)
{
SplineSelection.Clear();
s_PreRectSelectionElements.Clear();
}
else
SplineSelection.GetElements(splines, s_PreRectSelectionElements);
}
void UpdateSelection(Rect rect, IReadOnlyList<SplineInfo> splines)
{
//Get all elements in rect
s_SplineElementsBuffer.Clear();
for (int i = 0; i < splines.Count; ++i)
{
var splineData = splines[i];
for (int j = 0; j < splineData.Spline.Count; ++j)
if(!SplineSelection.HasActiveSplineSelection() || SplineSelection.Contains(splineData))
GetElementSelection(rect, splineData, j, s_SplineElementsBuffer);
}
foreach (var splineElement in s_SplineElementsBuffer)
{
//Compare current frame buffer with last frame's to find new additions/removals
var wasInRectLastFrame = s_SplineElementsCompareSet.Remove(splineElement);
if (m_Mode == Mode.Replace || m_Mode == Mode.Add)
{
var canAdd = m_Mode == Mode.Replace ? true : !s_PreRectSelectionElements.Contains(splineElement);
if (!wasInRectLastFrame && canAdd)
SplineSelection.Add(splineElement);
}
else if (m_Mode == Mode.Subtract && !wasInRectLastFrame)
{
SplineSelection.Remove(splineElement);
}
}
//Remaining spline elements from last frame are removed from selection (or added if mode is subtract)
foreach (var splineElement in s_SplineElementsCompareSet)
{
if (m_Mode == Mode.Replace || m_Mode == Mode.Add)
{
// If we're in Add mode, don't remove elements that were in select prior to rect selection
if (m_Mode == Mode.Add && s_PreRectSelectionElements.Contains(splineElement))
continue;
SplineSelection.Remove(splineElement);
}
else if (m_Mode == Mode.Subtract && s_PreRectSelectionElements.Contains(splineElement))
SplineSelection.Add(splineElement);
}
//Move current elements buffer to hash set for next frame compare
s_SplineElementsCompareSet.Clear();
foreach (var splineElement in s_SplineElementsBuffer)
s_SplineElementsCompareSet.Add(splineElement);
}
bool RefreshSelectionMode()
{
var modeBefore = m_Mode;
if (Event.current.shift)
m_Mode = Mode.Add;
else if (EditorGUI.actionKey)
m_Mode = Mode.Subtract;
else
m_Mode = Mode.Replace;
// Return true if the mode has changed
return m_Mode != modeBefore;
}
void GetElementSelection(Rect rect, SplineInfo splineInfo, int index, List<ISelectableElement> results)
{
var knot = splineInfo.Spline[index];
var localToWorld = splineInfo.LocalToWorld;
var worldKnot = knot.Transform(localToWorld);
Vector3 screenSpace = HandleUtility.WorldToGUIPointWithDepth(worldKnot.Position);
if (screenSpace.z > 0 && rect.Contains(screenSpace))
results.Add(new SelectableKnot(splineInfo, index));
var tangentIn = new SelectableTangent(splineInfo, index, BezierTangent.In);
if (SplineSelectionUtility.IsSelectable(tangentIn))
{
screenSpace = HandleUtility.WorldToGUIPointWithDepth(worldKnot.Position + math.rotate(worldKnot.Rotation, worldKnot.TangentIn));
if (screenSpace.z > 0 && rect.Contains(screenSpace))
results.Add(tangentIn);
}
var tangentOut = new SelectableTangent(splineInfo, index, BezierTangent.Out);
if (SplineSelectionUtility.IsSelectable(tangentOut))
{
screenSpace = HandleUtility.WorldToGUIPointWithDepth(worldKnot.Position + math.rotate(worldKnot.Rotation, worldKnot.TangentOut));
if (screenSpace.z > 0 && rect.Contains(screenSpace))
results.Add(tangentOut);
}
}
void EndSelection(Rect rect, IReadOnlyList<SplineInfo> splines)
{
m_Mode = m_InitialMode = Mode.None;
}
static Rect GetRectFromPoints(Vector2 a, Vector2 b)
{
Vector2 min = new Vector2(Mathf.Min(a.x, b.x), Mathf.Min(a.y, b.y));
Vector2 max = new Vector2(Mathf.Max(a.x, b.x), Mathf.Max(a.y, b.y));
return new Rect(min, max - min);
}
}
}

View File

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

View File

@@ -0,0 +1,68 @@
using System;
using UnityEngine;
using UnityEditor.SettingsManagement;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
static class SplineHandleSettings
{
[UserSetting]
static readonly Pref<bool> s_FlowDirectionEnabled = new Pref<bool>("Handles.FlowDirectionEnabled", true);
[UserSetting]
static readonly Pref<bool> s_ShowAllTangents = new Pref<bool>("Handles.ShowAllTangents", true);
static readonly Pref<bool> s_ShowKnotIndices = new Pref<bool>("Handles.ShowKnotIndices", false);
[UserSetting]
static UserSetting<bool> s_ShowMesh = new UserSetting<bool>(PathSettings.instance,"Handles.Debug.ShowMesh", false, SettingsScope.User);
[UserSetting]
static UserSetting<Color> s_MeshColor = new UserSetting<Color>(PathSettings.instance, "Handles.Debug.MeshColor", Color.white, SettingsScope.User);
[UserSetting]
static UserSetting<float> s_MeshSize = new UserSetting<float>(PathSettings.instance, "Handles.Debug.MeshSize", 0.1f, SettingsScope.User);
[UserSetting]
static UserSetting<int> s_MeshResolution = new UserSetting<int>(PathSettings.instance, "Handles.Debug.MeshResolution", SplineUtility.DrawResolutionDefault, SettingsScope.User);
[UserSettingBlock("Spline Mesh")]
static void HandleDebugPreferences(string searchContext)
{
EditorGUI.BeginChangeCheck();
s_MeshColor.value = SettingsGUILayout.SettingsColorField("Color", s_MeshColor, searchContext);
s_MeshSize.value = SettingsGUILayout.SettingsSlider("Size", s_MeshSize, 0.01f, 1f, searchContext);
s_MeshResolution.value = SettingsGUILayout.SettingsSlider("Resolution", s_MeshResolution, 4, 100, searchContext);
if(EditorGUI.EndChangeCheck())
SceneView.RepaintAll();
}
public static bool FlowDirectionEnabled
{
get => s_FlowDirectionEnabled;
set => s_FlowDirectionEnabled.SetValue(value);
}
public static bool ShowAllTangents
{
get => s_ShowAllTangents;
set => s_ShowAllTangents.SetValue(value);
}
public static bool ShowKnotIndices
{
get => s_ShowKnotIndices;
set => s_ShowKnotIndices.SetValue(value);
}
public static bool ShowMesh
{
get => s_ShowMesh;
set => s_ShowMesh.SetValue(value);
}
public static Color SplineMeshColor => s_MeshColor;
public static float SplineMeshSize => s_MeshSize;
public static int SplineMeshResolution => s_MeshResolution;
}
}

View File

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

View File

@@ -0,0 +1,464 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;
using UnityEngine.Rendering;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
/// <summary>
/// This class provides the ability to draw a handle for a spline.
/// </summary>
public static class SplineHandles
{
/// <summary>
/// The scope used to draw a spline. This is managing several purposes when using SplineHandles.DrawSomething().
/// This ensure selection is working properly, and that hovering an element is highlighting the correct related
/// elements (for instance hovering a tangent highlights the opposite one when needed and the knot as well).
/// </summary>
public class SplineHandleScope : IDisposable
{
int m_NearestControl;
/// <summary>
/// Defines a new scope to draw spline elements in.
/// </summary>
public SplineHandleScope()
{
m_NearestControl = HandleUtility.nearestControl;
Clear();
SplineHandleUtility.minElementId = GUIUtility.GetControlID(FocusType.Passive);
}
/// <summary>
/// Called automatically when the `SplineHandleScope` is disposed.
/// </summary>
public void Dispose()
{
SplineHandleUtility.maxElementId = GUIUtility.GetControlID(FocusType.Passive);
var evtType = Event.current.type;
if ( (evtType == EventType.MouseMove || evtType == EventType.Layout)
&& HandleUtility.nearestControl == m_NearestControl)
SplineHandleUtility.ResetLastHoveredElement();
}
}
/// <summary>
/// The color of sections of spline curve handles that are behind objects in the Scene view.
/// </summary>
public static Color lineBehindColor => SplineHandleUtility.lineBehindColor;
/// <summary>
/// The color of sections of spline curves handles that are in front of objects in the Scene view.
/// </summary>
public static Color lineColor => SplineHandleUtility.lineColor;
/// <summary>
/// The color of tangent handles for a spline.
/// </summary>
public static Color tangentColor => SplineHandleUtility.tangentColor;
/// <summary>
/// The distance to pick a spline knot, tangent, or curve handle at.
/// </summary>
public static float pickingDistance => SplineHandleUtility.pickingDistance;
static List<int> s_ControlIDs = new();
static List<int> s_CurveIDs = new();
static readonly List<SelectableKnot> k_KnotBuffer = new ();
static Dictionary<SelectableKnot, int> s_KnotsIDs = new ();
// todo Tools.viewToolActive should be handling the modifier check, but 2022.2 broke this
internal static bool ViewToolActive()
{
return Tools.viewToolActive || Tools.current == Tool.View || (Event.current.modifiers & EventModifiers.Alt) == EventModifiers.Alt;
}
static void Clear()
{
s_CurveIDs.Clear();
s_KnotsIDs.Clear();
}
/// <summary>
/// Creates handles for a set of splines. These handles display the knots, tangents, and segments of a spline.
/// These handles support selection and the direct manipulation of spline elements.
/// </summary>
/// <param name="splines">The set of splines to draw handles for.</param>
public static void DoHandles(IReadOnlyList<SplineInfo> splines)
{
Profiler.BeginSample("SplineHandles.DoHandles");
using (new SplineHandleScope())
{
// Drawing done in two separate passes to make sure the curves are drawn behind the spline elements.
// Draw the curves.
for (int i = 0; i < splines.Count; ++i)
{
DoSegmentsHandles(splines[i]);
}
DoKnotsAndTangentsHandles(splines);
}
Profiler.EndSample();
}
internal static bool IsCurveId(int id)
{
return s_CurveIDs.Contains(id);
}
/// <summary>
/// Creates knot and tangent handles for a spline. Call `DoKnotsAndTangentsHandles` in a `SplineHandleScope`.
/// This method is used internally by `DoHandles`.
/// </summary>
/// <param name="spline">The spline to create knot and tangent handles for.</param>
public static void DoKnotsAndTangentsHandles(SplineInfo spline)
{
SplineHandleUtility.UpdateElementColors();
KnotHandles.ClearVisibleKnots();
// Draw the spline elements.
DrawSplineElements(spline);
//Drawing knots on top of all other elements and above other splines
KnotHandles.DrawVisibleKnots();
}
/// <summary>
/// Creates knot and tangent handles for multiple splines. Call `DoKnotsAndTangentsHandles` in a `SplineHandleScope`.
/// This method is used internally by `DoHandles`.
/// </summary>
/// <param name="splines">The splines to create knot and tangent handles for.</param>
public static void DoKnotsAndTangentsHandles(IReadOnlyList<SplineInfo> splines)
{
SplineHandleUtility.UpdateElementColors();
KnotHandles.ClearVisibleKnots();
// Draw the spline elements.
for (int i = 0; i < splines.Count; ++i)
DrawSplineElements(splines[i]);
//Drawing knots on top of all other elements and above other splines
KnotHandles.DrawVisibleKnots();
}
/// <summary>
/// Creates segment handles for a spline. Call `DoCurvesHandles` in a `SplineHandleScope`.
/// This method is used internally by `DrawHandles`.
/// </summary>
/// <param name="splineInfo">The splineInfo of the spline to draw knots and tangents for.</param>
public static void DoSegmentsHandles(SplineInfo splineInfo)
{
var spline = splineInfo.Spline;
if (spline == null || spline.Count < 2)
return;
var localToWorld = splineInfo.LocalToWorld;
// If the spline isn't closed, skip the last index of the spline
int lastIndex = spline.Closed ? spline.Count - 1 : spline.Count - 2;
if (SplineHandleSettings.ShowMesh)
{
using (var nativeSpline = new NativeSpline(spline, localToWorld))
using (var mesh = new SplineMeshHandle<NativeSpline>())
using (new ZTestScope(UnityEngine.Rendering.CompareFunction.Less))
{
mesh.Do(nativeSpline, SplineHandleSettings.SplineMeshSize, SplineHandleSettings.SplineMeshColor, SplineHandleSettings.SplineMeshResolution);
}
}
s_ControlIDs.Clear();
for (int idIndex = 0; idIndex < lastIndex + 1; ++idIndex)
{
var id = GUIUtility.GetControlID(FocusType.Passive);
s_ControlIDs.Add(id);
s_CurveIDs.Add(id);
}
var drawHandlesAsActive = !SplineSelection.HasActiveSplineSelection() || SplineSelection.Contains(splineInfo);
//Draw all the curves at once
SplineCacheUtility.GetCachedPositions(spline, out var positions);
using (new Handles.DrawingScope(SplineHandleUtility.lineColor, localToWorld))
{
using (new ZTestScope(CompareFunction.Less))
Handles.DrawAAPolyLine(SplineHandleUtility.denseLineAATex, 4f, positions);
}
using (new Handles.DrawingScope(SplineHandleUtility.lineBehindColor, localToWorld))
{
using (new ZTestScope(CompareFunction.Greater))
Handles.DrawAAPolyLine(SplineHandleUtility.denseLineAATex, 4f, positions);
}
if (drawHandlesAsActive)
{
for (int curveIndex = 0; curveIndex < lastIndex + 1; ++curveIndex)
{
if (SplineHandleSettings.FlowDirectionEnabled && Event.current.type == EventType.Repaint)
{
var curve = spline.GetCurve(curveIndex).Transform(localToWorld);
CurveHandles.DrawFlow(curve, spline, curveIndex);
}
}
}
for (int curveIndex = 0; curveIndex < lastIndex + 1; ++curveIndex)
{
CurveHandles.DrawWithHighlight(
s_ControlIDs[curveIndex],
spline,
curveIndex,
localToWorld,
new SelectableKnot(splineInfo, curveIndex),
new SelectableKnot(splineInfo, SplineUtility.NextIndex(curveIndex, spline.Count, spline.Closed)),
drawHandlesAsActive);
}
SplineHandleUtility.canDrawOnCurves = true;
}
static void DrawSplineElements(SplineInfo splineInfo)
{
var spline = splineInfo.Spline;
var drawHandlesAsActive = !SplineSelection.HasActiveSplineSelection() || SplineSelection.Contains(splineInfo);
if (drawHandlesAsActive)
{
for (int knotIndex = 0; knotIndex < spline.Count; ++knotIndex)
DrawKnotWithTangentsHandles_Internal(new SelectableKnot(splineInfo, knotIndex));
}
else
{
for (int knotIndex = 0; knotIndex < spline.Count; ++knotIndex)
KnotHandles.DrawInformativeKnot(new SelectableKnot(splineInfo, knotIndex));
}
}
/// <summary>
/// Creates handles for a knot and its tangents if those tangents are modifiable.
/// These handles support the selection and direct manipulation of spline elements.
/// Call `DoKnotWithTangentsHandles` in a `SplineHandleScope`.
/// </summary>
/// <param name="knot">The knot to draw handles for.</param>
public static void DoKnotWithTangentsHandles(SelectableKnot knot)
{
KnotHandles.ClearVisibleKnots();
DrawKnotWithTangentsHandles_Internal(knot);
KnotHandles.DrawVisibleKnots();
}
static void DrawKnotWithTangentsHandles_Internal(SelectableKnot knot)
{
var splineInfo = knot.SplineInfo;
if (SplineUtility.AreTangentsModifiable(splineInfo.Spline.GetTangentMode(knot.KnotIndex)))
DoTangentsHandles(knot);
DrawKnotHandles_Internal(knot);
}
/// <summary>
/// Create handles for a knot. These handles the support selection and direct manipulation of spline elements.
/// Call `DoKnotHandles` in a `SplineHandleScope`.
/// </summary>
/// <param name="knot">The knot to draw handles for.</param>
public static void DoKnotHandles(SelectableKnot knot)
{
KnotHandles.ClearVisibleKnots();
DrawKnotHandles_Internal(knot);
KnotHandles.DrawVisibleKnots();
}
static void DrawKnotHandles_Internal(SelectableKnot knot)
{
var id = GetKnotID(knot);
SelectionHandle(id, knot);
KnotHandles.Draw(id, knot);
}
/// <summary>
/// Create handles for a knot's tangents if those tangents are modifiable. `DoTangentsHandles` does not create handles for the knot.
/// These handles support the selection and direct manipulation of the spline elements.
/// Call `DoTangentsHandles` in a `SplineHandleScope`.
/// </summary>
/// <param name="knot">The knot to draw tangent handles for.</param>
public static void DoTangentsHandles(SelectableKnot knot)
{
if(!knot.IsValid())
return;
var splineInfo = knot.SplineInfo;
var spline = splineInfo.Spline;
var knotIndex = knot.KnotIndex;
var tangentIn = new SelectableTangent(splineInfo, knotIndex, BezierTangent.In);
var tangentOut = new SelectableTangent(splineInfo, knotIndex, BezierTangent.Out);
var controlIdIn = GUIUtility.GetControlID(FocusType.Passive);
var controlIdOut = GUIUtility.GetControlID(FocusType.Passive);
// Tangent In
if (GUIUtility.hotControl == controlIdIn || SplineHandleUtility.ShouldShowTangent(tangentIn) && (spline.Closed || knotIndex != 0))
{
SelectionHandle(controlIdIn, tangentIn);
TangentHandles.Draw(controlIdIn, tangentIn);
}
// Tangent Out
if (GUIUtility.hotControl == controlIdOut || SplineHandleUtility.ShouldShowTangent(tangentOut) && (spline.Closed || knotIndex + 1 != spline.Count))
{
SelectionHandle(controlIdOut, tangentOut);
TangentHandles.Draw(controlIdOut, tangentOut);
}
}
static int GetKnotID(SelectableKnot knot)
{
EditorSplineUtility.GetKnotLinks(knot, k_KnotBuffer);
//If a linked knot as already been assigned, return the same id
if (s_KnotsIDs.ContainsKey(k_KnotBuffer[0]))
return s_KnotsIDs[k_KnotBuffer[0]];
//else compute a new id and record it
var id = GUIUtility.GetControlID(FocusType.Passive);
s_KnotsIDs.Add(k_KnotBuffer[0], id);
return id;
}
static void SelectionHandle<T>(int id, T element)
where T : struct, ISelectableElement
{
Event evt = Event.current;
EventType eventType = evt.GetTypeForControl(id);
switch (eventType)
{
case EventType.Layout:
case EventType.MouseMove:
if (!ViewToolActive())
{
HandleUtility.AddControl(id, SplineHandleUtility.DistanceToCircle(element.Position, SplineHandleUtility.pickingDistance));
if (GUIUtility.hotControl == 0 && HandleUtility.nearestControl == id)
SplineHandleUtility.SetLastHoveredElement(element, id);
}
break;
case EventType.MouseDown:
if (HandleUtility.nearestControl == id && evt.button == 0)
{
GUIUtility.hotControl = id;
evt.Use();
DirectManipulation.BeginDrag(element.Position, EditorSplineUtility.GetElementRotation(element));
}
break;
case EventType.MouseDrag:
if (GUIUtility.hotControl == id)
{
EditorSplineUtility.RecordObject(element.SplineInfo, "Move Knot");
var pos = TransformOperation.ApplySmartRounding(DirectManipulation.UpdateDrag(id));
if (element is SelectableTangent tangent)
EditorSplineUtility.ApplyPositionToTangent(tangent, pos);
else
element.Position = pos;
GUI.changed = true;
evt.Use();
}
break;
case EventType.MouseUp:
if (GUIUtility.hotControl == id && evt.button == 0)
{
if (!DirectManipulation.IsDragging)
SplineSelectionUtility.HandleSelection(element);
DirectManipulation.EndDrag();
GUI.changed = true;
evt.Use();
GUIUtility.hotControl = 0;
}
break;
case EventType.Repaint:
DirectManipulation.DrawHandles(id, element.Position);
break;
}
}
/// <summary>
/// Draws a handle for a spline.
/// </summary>
/// <param name="spline">The target spline.</param>
/// <typeparam name="T">A type implementing ISpline.</typeparam>
public static void DoSpline<T>(T spline) where T : ISpline => DoSpline(-1, spline);
/// <summary>
/// Draws a handle for a spline.
/// </summary>
/// <param name="controlID">The spline mesh controlID.</param>
/// <param name="spline">The target spline.</param>
/// <typeparam name="T">A type implementing ISpline.</typeparam>
public static void DoSpline<T>(int controlID, T spline) where T : ISpline
{
for(int i = 0; i < spline.GetCurveCount(); ++i)
CurveHandles.Draw(controlID, spline.GetCurve(i));
}
/// <summary>
/// Draws a handle for a <see cref="BezierCurve"/>.
/// </summary>
/// <param name="curve">The <see cref="BezierCurve"/> to create handles for.</param>
public static void DoCurve(BezierCurve curve) => CurveHandles.Draw(-1, curve);
/// <summary>
/// Draws a handle for a <see cref="BezierCurve"/>.
/// </summary>
/// <param name="controlID">The spline mesh controlID.</param>
/// <param name="curve">The <see cref="BezierCurve"/> to create handles for.</param>
public static void DoCurve(int controlID, BezierCurve curve) => CurveHandles.Draw(controlID, curve);
/// <summary>
/// Draws handles for a knot. These handles are drawn only during repaint events and not on selection.
/// </summary>
/// <param name="knot">The <see cref="SelectableKnot"/> to create handles for.</param>
/// <param name="selected">Set to true to draw the knot handle as a selected element.</param>
/// <param name="hovered">Set to true to draw the knot handle as a hovered element.</param>
public static void DrawKnot(SelectableKnot knot, bool selected = false, bool hovered = false)
=> DrawKnot(-1, knot, selected, hovered);
/// <summary>
/// Draws handles for a knot. These handles are drawn only during repaint events and not on selection.
/// </summary>
/// <param name="controlID">The controlID of the tangent to create handles for.</param>
/// <param name="knot">The <see cref="SelectableKnot"/> to create handles for.</param>
/// <param name="selected">Set to true to draw the knot handle as a selected element.</param>
/// <param name="hovered">Set to true to draw the knot handle as a hovered element.</param>
public static void DrawKnot(int controlID, SelectableKnot knot, bool selected = false, bool hovered = false)
{
KnotHandles.Do(controlID, knot, selected, hovered);
}
/// <summary>
/// Draws handles for a tangent. These handles are drawn only during repaint events and not on selection.
/// </summary>
/// <param name="tangent">The <see cref="SelectableTangent"/> to create handles for.</param>
/// <param name="selected">Set to true to draw the tangent handle as a selected element.</param>
/// <param name="hovered">Set to true to draw the tangent handle as a hovered element.</param>
public static void DrawTangent(SelectableTangent tangent, bool selected = false, bool hovered = false) => DrawTangent(-1, tangent, selected, hovered);
/// <summary>
/// Draws handles for a tangent. These handles are drawn only during repaint events and not on selection.
/// </summary>
/// <param name="controlID">The controlID of the tangent to create handles for.</param>
/// <param name="tangent">The <see cref="SelectableTangent"/> to create handles for.</param>
/// <param name="selected">Set to true to draw the tangent handle as a selected element.</param>
/// <param name="hovered">Set to true to draw the tangent handle as a hovered element.</param>
public static void DrawTangent(int controlID, SelectableTangent tangent, bool selected = false, bool hovered = false)
{
TangentHandles.Do(controlID, tangent, selected, hovered);
}
}
}

View File

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

View File

@@ -0,0 +1,188 @@
using System;
using UnityEngine;
using UnityEngine.Splines;
using Object = UnityEngine.Object;
namespace UnityEditor.Splines
{
/// <summary>
/// Creates a cylinder mesh along a spline.
/// </summary>
/// <typeparam name="T">The type of ISpline.</typeparam>
public class SplineMeshHandle<T> : IDisposable where T : ISpline
{
class SplineMeshDrawingScope : IDisposable
{
Material m_Material;
int m_HandleZTestId;
int m_BlendSrcModeId;
int m_BlendDstModeId;
float m_PreviousZTest;
int m_PreviousBlendSrcMode;
int m_PreviousBlendDstMode;
public SplineMeshDrawingScope(Material material, Color color)
{
Shader.SetGlobalColor("_HandleColor", color);
Shader.SetGlobalFloat("_HandleSize", 1f);
Shader.SetGlobalMatrix("_ObjectToWorld", Handles.matrix);
if (material == null)
{
m_Material = HandleUtility.handleMaterial;
m_HandleZTestId = Shader.PropertyToID("_HandleZTest");
m_BlendSrcModeId = Shader.PropertyToID("_BlendSrcMode");
m_BlendDstModeId = Shader.PropertyToID("_BlendDstMode");
m_PreviousZTest = m_Material.GetFloat(m_HandleZTestId);
m_PreviousBlendSrcMode = m_Material.GetInt(m_BlendSrcModeId);
m_PreviousBlendDstMode = m_Material.GetInt(m_BlendDstModeId);
m_Material.SetFloat(m_HandleZTestId, (float)Handles.zTest);
m_Material.SetInt(m_BlendSrcModeId, (int)UnityEngine.Rendering.BlendMode.One);
m_Material.SetInt(m_BlendDstModeId, (int)UnityEngine.Rendering.BlendMode.One);
m_Material.SetPass(0);
}
else
material.SetPass(0);
}
public void Dispose()
{
if (m_Material != null)
{
m_Material.SetFloat(m_HandleZTestId, m_PreviousZTest);
m_Material.SetInt(m_BlendSrcModeId, m_PreviousBlendSrcMode);
m_Material.SetInt(m_BlendDstModeId, m_PreviousBlendDstMode);
}
}
}
Mesh m_Mesh;
Material m_Material;
/// <summary>
/// Creates a new mesh handle. This class implements IDisposable to clean up allocated mesh resources. Call
/// <see cref="Dispose"/> when you are finished with the instance.
/// </summary>
public SplineMeshHandle()
{
m_Mesh = new Mesh()
{
hideFlags = HideFlags.HideAndDontSave
};
m_Material = null;
}
/// <summary>
/// Create a new mesh handle. This class implements IDisposable to clean up allocated mesh resources. Call
/// <see cref="Dispose"/> when you are finished with the instance.
/// </summary>
/// <param name="material">The material to render the cylinder mesh with.</param>
public SplineMeshHandle(Material material) : base()
{
m_Material = material;
}
/// <summary>
/// The material to render this mesh with. If null, a default material is used.
/// </summary>
public Material material
{
get => m_Material;
set => m_Material = value;
}
/// <summary>
/// Draws a 3D mesh from a spline.
/// </summary>
/// <param name="spline">The target spline.</param>
/// <param name="size">The width to use for the spline mesh.</param>
/// <param name="color">The color to use for the spline mesh in normal mode.</param>
/// <param name="resolution">The resolution to use for the mesh, defines the number of segments per unit
/// with default value of <see cref="SplineUtility.DrawResolutionDefault"/>.</param>
public void Do(T spline, float size, Color color, int resolution = SplineUtility.DrawResolutionDefault)
{
if(Event.current.type != EventType.Repaint)
return;
Do(-1, spline, size, color, resolution);
}
/// <summary>
/// Draws a 3D mesh handle from a spline.
/// </summary>
/// <param name="controlID">The spline mesh controlID.</param>
/// <param name="spline">The target spline.</param>
/// <param name="size">The width to use for the spline mesh.</param>
/// <param name="color">The color to use for the spline mesh in normal mode.</param>
/// <param name="resolution">The resolution to use for the mesh, defines the number of segments per unit
/// with default value of <see cref="SplineUtility.DrawResolutionDefault"/>.</param>
public void Do(int controlID, T spline, float size, Color color, int resolution = SplineUtility.DrawResolutionDefault)
{
using (new Handles.DrawingScope(color))
Do(controlID, spline, size, resolution);
}
/// <summary>
/// Draws a 3D mesh from a spline.
/// </summary>
/// <param name="spline">The target spline.</param>
/// <param name="size">The width to use for the spline mesh.</param>
/// <param name="resolution">The resolution to use for the mesh, defines the number of segments per unit
/// with default value of <see cref="SplineUtility.DrawResolutionDefault"/>.</param>
public void Do(T spline, float size, int resolution = SplineUtility.DrawResolutionDefault)
{
if (Event.current.type != EventType.Repaint)
return;
Do(-1, spline, size, resolution);
}
/// <summary>
/// Draws a 3D mesh handle from a spline.
/// </summary>
/// <param name="controlID">The spline mesh controlID.</param>
/// <param name="spline">The target spline.</param>
/// <param name="size">The width to use for the spline mesh.</param>
/// <param name="resolution">The resolution to use for the mesh, defines the number of segments per unit
/// with default value of <see cref="SplineUtility.DrawResolutionDefault"/>.</param>
public void Do(int controlID, T spline, float size, int resolution = SplineUtility.DrawResolutionDefault)
{
var evt = Event.current;
switch (evt.type)
{
case EventType.MouseMove:
var ray = HandleUtility.GUIPointToWorldRay(evt.mousePosition);
HandleUtility.AddControl(controlID, SplineUtility.GetNearestPoint(spline, ray, out _, out _));
break;
case EventType.Repaint:
var segments = SplineUtility.GetSubdivisionCount(spline.GetLength(), resolution);
SplineMesh.Extrude(spline, m_Mesh, size, 8, segments, !spline.Closed);
var color = GUIUtility.hotControl == controlID
? Handles.selectedColor
: HandleUtility.nearestControl == controlID
? Handles.preselectionColor
: Handles.color;
using (new SplineMeshDrawingScope(m_Material, color))
Graphics.DrawMeshNow(m_Mesh, Handles.matrix);
break;
}
}
/// <summary>
/// Destroys the 3D mesh.
/// </summary>
public void Dispose() => Object.DestroyImmediate(m_Mesh);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b7822ae247afa5945befac0bc63be0bb
timeCreated: 1654094147

View File

@@ -0,0 +1,238 @@
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
static class TangentHandles
{
const float k_ColorAlphaFactor = 0.3f;
const float k_TangentLineWidthDefault = 2f;
const float k_TangentLineWidthHover = 3.5f;
const float k_TangentLineWidthSelected = 4.5f;
const float k_TangentStartOffsetFromKnot = 0.22f;
const float k_TangentEndOffsetFromHandle = 0.11f;
const float k_TangentHandleWidth = 2f;
const float k_TangentRotWidthDefault = 1.5f;
const float k_TangentRotDiscWidth = 3f;
internal static void Do(int controlId, SelectableTangent tangent, bool selected = false, bool hovered = false)
{
var owner = tangent.Owner;
Draw(
controlId,
tangent.Position,
EditorSplineUtility.GetElementRotation(math.length(tangent.LocalPosition) > 0 ? (ISelectableElement)tangent : tangent.Owner),
owner.Position,
selected,
false,
hovered,
false,
owner.Mode,
true);
}
internal static void DrawInformativeTangent(SelectableTangent tangent, bool active = true)
{
DrawInformativeTangent(tangent.Position, tangent.Owner.Position, active);
}
internal static void DrawInformativeTangent(Vector3 position, Vector3 knotPosition, bool active = true)
{
if (Event.current.type != EventType.Repaint)
return;
var tangentColor = SplineHandleUtility.elementColor;
if (!active)
tangentColor = Handles.secondaryColor;
var tangentArmColor = tangentColor == SplineHandleUtility.elementColor
? SplineHandleUtility.tangentColor
: tangentColor;
using (new ColorScope(tangentArmColor))
{
var toTangent = position - knotPosition;
var toTangentNorm = math.normalize(toTangent);
var length = math.length(toTangent);
var knotHandleOffset = HandleUtility.GetHandleSize(knotPosition) * k_TangentStartOffsetFromKnot;
length = Mathf.Max(0f, length - knotHandleOffset);
knotPosition += (Vector3)toTangentNorm * knotHandleOffset;
SplineHandleUtility.DrawLineWithWidth(knotPosition, knotPosition + (Vector3)toTangentNorm * length, k_TangentLineWidthDefault);
}
}
internal static void Draw(Vector3 position, Vector3 knotPosition, float3 normal, bool active = true)
{
var knotToTangentDirection = position - knotPosition;
var rotation = quaternion.LookRotationSafe(knotToTangentDirection, normal);
Draw(-1, position, rotation, knotPosition, false, false, false, TangentMode.Broken, active);
}
internal static void Draw(int controlId, SelectableTangent tangent, bool active = true)
{
var (pos, rot) = SplineCacheUtility.GetTangentPositionAndRotation(tangent);
var owner = tangent.Owner;
Draw(
controlId,
pos,
rot,
owner.Position,
SplineSelection.Contains(tangent),
SplineSelection.Contains(tangent.OppositeTangent),
SplineHandleUtility.IsLastHoveredElement(tangent.OppositeTangent),
owner.Mode,
active);
}
static void Draw(int controlId, Vector3 position, Quaternion rotation, Vector3 knotPosition, bool selected, bool oppositeSelected, bool oppositeHovered, TangentMode mode, bool active)
{
var hovered = SplineHandleUtility.IsHoverAvailableForSplineElement() && SplineHandleUtility.IsElementHovered(controlId);
Draw(controlId, position, rotation, knotPosition, selected, oppositeSelected, hovered, oppositeHovered, mode, active);
}
static void Draw(int controlId, Vector3 position, Quaternion rotation, Vector3 knotPosition, bool selected, bool oppositeSelected, bool hovered, bool oppositeHovered, TangentMode mode, bool active)
{
if (Event.current.GetTypeForControl(controlId) != EventType.Repaint)
return;
var size = HandleUtility.GetHandleSize(position);
var tangentColor = SplineHandleUtility.elementColor;
if (hovered)
tangentColor = SplineHandleUtility.elementPreselectionColor;
else if (selected)
tangentColor = SplineHandleUtility.elementSelectionColor;
if (!active)
tangentColor = Handles.secondaryColor;
var tangentArmColor = tangentColor == SplineHandleUtility.elementColor ?
SplineHandleUtility.tangentColor :
tangentColor;
if (mode == TangentMode.Mirrored)
{
if(oppositeHovered)
tangentArmColor = SplineHandleUtility.elementPreselectionColor;
else if(tangentArmColor == SplineHandleUtility.tangentColor && oppositeSelected)
tangentArmColor =SplineHandleUtility.elementSelectionColor;
}
var rotationDiscWidth = k_TangentRotWidthDefault;
if (hovered)
rotationDiscWidth = k_TangentRotDiscWidth;
using (new ZTestScope(CompareFunction.Less))
{
// Draw tangent arm.
using (new ColorScope(tangentArmColor))
DrawTangentArm(position, knotPosition, size, mode, selected, hovered, oppositeSelected, oppositeHovered);
// Draw tangent shape.
using (new Handles.DrawingScope(tangentColor, Matrix4x4.TRS(position, rotation, Vector3.one)))
DrawTangentShape(size, selected);
}
using (new ZTestScope(CompareFunction.Greater))
{
// Draw tangent arm.
var newTangentArmColor = new Color(tangentArmColor.r, tangentArmColor.g, tangentArmColor.b, tangentArmColor.a * k_ColorAlphaFactor);
using (new ColorScope(newTangentArmColor))
DrawTangentArm(position, knotPosition, size, mode, selected, hovered, oppositeSelected, oppositeHovered);
// Draw tangent shape.
var newDiscColor = new Color(tangentColor.r, tangentColor.g, tangentColor.b, tangentColor.a * k_ColorAlphaFactor);
using (new Handles.DrawingScope(newDiscColor, Matrix4x4.TRS(position, rotation, Vector3.one)))
DrawTangentShape(size, selected);
}
// Draw tangent disc on hover.
if (hovered)
{
var tangentHandleOffset = size * k_TangentEndOffsetFromHandle;
using (new ZTestScope(CompareFunction.Less))
{
using (new Handles.DrawingScope(tangentColor, Matrix4x4.TRS(position, rotation, Vector3.one)))
SplineHandleUtility.DrawAAWireDisc(Vector3.zero, Vector3.up, tangentHandleOffset, rotationDiscWidth);
}
using (new ZTestScope(CompareFunction.Greater))
{
var newDiscColor = new Color(tangentColor.r, tangentColor.g, tangentColor.b, tangentColor.a * k_ColorAlphaFactor);
using (new Handles.DrawingScope(newDiscColor, Matrix4x4.TRS(position, rotation, Vector3.one)))
SplineHandleUtility.DrawAAWireDisc(Vector3.zero, Vector3.up, tangentHandleOffset, rotationDiscWidth);
}
}
}
static void DrawTangentArm(Vector3 position, Vector3 knotPosition, float size, TangentMode mode, bool selected, bool hovered, bool oppositeSelected, bool oppositeHovered)
{
var width = k_TangentLineWidthDefault;
if (!DirectManipulation.IsDragging)
{
if (selected || (mode != TangentMode.Broken && oppositeSelected))
width = k_TangentLineWidthSelected;
else if (hovered || (mode != TangentMode.Broken && oppositeHovered))
width = k_TangentLineWidthHover;
}
var startPos = knotPosition;
var toTangent = position - knotPosition;
var toTangentNorm = math.normalize(toTangent);
var length = math.length(toTangent);
var knotHandleSize = HandleUtility.GetHandleSize(startPos);
var knotHandleOffset = knotHandleSize * k_TangentStartOffsetFromKnot;
var tangentHandleOffset = size * k_TangentEndOffsetFromHandle;
// Reduce the length slightly, so that there's some space between tangent line endings and handles.
length = Mathf.Max(0f, length - knotHandleOffset - tangentHandleOffset);
startPos += (Vector3)toTangentNorm * knotHandleOffset;
SplineHandleUtility.DrawLineWithWidth(startPos + (Vector3)toTangentNorm * length, startPos, width, SplineHandleUtility.denseLineAATex);
}
static void DrawTangentShape(float size, bool selected)
{
var midVector = new Vector3(-.5f, 0, .5f);
if (selected)
{
var factor = 0.7f;
var radius = (selected ? SplineHandleUtility.knotDiscRadiusFactorSelected : SplineHandleUtility.knotDiscRadiusFactorHover) * size;
// As Handles.DrawAAConvexPolygon has no thickness parameter, we're drawing a AA Polyline here so that the polygon has thickness when viewed from a shallow angle.
Handles.DrawAAPolyLine(SplineHandleUtility.denseLineAATex,
k_TangentHandleWidth,
factor * radius * midVector,
factor * radius * Vector3.forward,
factor * radius * Vector3.right,
-factor * radius * Vector3.forward,
-factor * radius * Vector3.right,
factor * radius * midVector);
Handles.DrawAAConvexPolygon(
radius * midVector,
radius * Vector3.forward,
radius * Vector3.right,
-radius * Vector3.forward,
-radius * Vector3.right,
radius * midVector);
}
else
{
var radius = SplineHandleUtility.knotDiscRadiusFactorDefault * size;
//Starting the polyline in the middle of a segment and not to a corner to get an invisible connection.
//Otherwise the connection is really visible in the corner as a small part is missing there.
Handles.DrawAAPolyLine(SplineHandleUtility.denseLineAATex,
k_TangentHandleWidth,
radius * midVector,
radius * Vector3.forward,
radius * Vector3.right,
-radius * Vector3.forward,
-radius * Vector3.right,
radius * midVector);
}
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,302 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Splines;
using Object = UnityEngine.Object;
namespace UnityEditor.Splines
{
static class CopyPaste
{
// JSONUtility needs a root object to serialize
[Serializable]
class CopyPasteBuffer
{
public SerializedSpline[] Splines;
public SerializedLink[] Links;
}
[Serializable]
struct SerializedKnot
{
public BezierKnot Knot;
public TangentMode Mode;
public float Tension;
public SerializedKnot(SelectableKnot knot)
{
Knot = knot.GetBezierKnot(false);
Mode = knot.Mode;
Tension = knot.Tension;
}
}
[Serializable]
class SerializedSpline
{
public float4x4 Transform;
public bool Closed;
public SerializedKnot[] Knots;
}
[Serializable]
class SerializedLink
{
public SplineKnotIndex[] Indices;
}
public static bool IsSplineCopyBuffer(string contents)
{
if (string.IsNullOrEmpty(contents))
return false;
var buffer = new CopyPasteBuffer();
try
{
EditorJsonUtility.FromJsonOverwrite(contents, buffer);
}
catch (ArgumentException)
{
return false;
}
return buffer.Splines?.Length > 0;
}
static int CompareKnot(SelectableKnot a, SelectableKnot b)
{
var compareTarget = (int)math.sign(a.SplineInfo.Object.GetInstanceID() - b.SplineInfo.Object.GetInstanceID());
if (compareTarget != 0)
return compareTarget;
var compareSpline = (int)math.sign(a.SplineInfo.Index - b.SplineInfo.Index);
if (compareSpline != 0)
return compareSpline;
return (int)math.sign(a.KnotIndex - b.KnotIndex);
}
public static string Copy(IEnumerable<SelectableKnot> selection)
{
SerializedKnot[] ToArray(SelectableKnot[] original)
{
var result = new SerializedKnot[original.Length];
for (int i = 0; i < result.Length; ++i)
result[i] = new SerializedKnot(original[i]);
return result;
}
void Flatten(List<SelectableKnot[]> arrays, List<SelectableKnot> results)
{
results.Clear();
foreach (var knotArray in arrays)
results.AddRange(knotArray);
}
Dictionary<SelectableKnot, SplineKnotIndex> knotToSerializedIndex = new Dictionary<SelectableKnot, SplineKnotIndex>();
List<SerializedSpline> splines = new List<SerializedSpline>();
List<SelectableKnot> originalKnots = new List<SelectableKnot>(selection);
var connectedKnots = GetConnectedKnots(originalKnots);
foreach (var connectedKnotArray in connectedKnots)
{
// Skip Orphan Knots
if (connectedKnotArray.Length < 2)
continue;
var splineInfo = connectedKnotArray[0].SplineInfo;
splines.Add(new SerializedSpline
{
Closed = splineInfo.Spline.Closed && connectedKnotArray.Length == splineInfo.Spline.Count,
Knots = ToArray(connectedKnotArray),
Transform = splineInfo.LocalToWorld
});
for (int i = 0; i < connectedKnotArray.Length; ++i)
knotToSerializedIndex.Add(connectedKnotArray[i], new SplineKnotIndex(splines.Count - 1, i));
}
// Add the links
List<SplineKnotIndex> indices = new List<SplineKnotIndex>();
List<SerializedLink> links = new List<SerializedLink>();
List<SelectableKnot> knots = new List<SelectableKnot>();
// Update the original knots array with the removal of orphan knots
Flatten(connectedKnots, originalKnots);
foreach (var originalKnot in originalKnots)
{
EditorSplineUtility.GetKnotLinks(originalKnot, knots);
indices.Clear();
foreach (var knot in knots)
{
if (knotToSerializedIndex.TryGetValue(knot, out var index))
{
indices.Add(index);
//Remove the pair to ensure we don't get duplicates for every knot in the same link
knotToSerializedIndex.Remove(knot);
}
}
// Only serialized the link if at least 2 copied knots were linked together
if (indices.Count >= 2)
links.Add(new SerializedLink {Indices = indices.ToArray()});
}
if (splines.Count == 0)
return string.Empty;
CopyPasteBuffer buffer = new CopyPasteBuffer
{
Splines = splines.ToArray(),
Links = links.ToArray(),
};
return EditorJsonUtility.ToJson(buffer);
}
static List<SelectableKnot[]> GetConnectedKnots(List<SelectableKnot> knots)
{
if (knots.Count == 0)
return new List<SelectableKnot[]>();
knots.Sort(CompareKnot);
List<SelectableKnot[]> results = new List<SelectableKnot[]>();
List<SelectableKnot> connected = new List<SelectableKnot> { knots[0] };
for (int i = 1; i < knots.Count; ++i)
{
var previous = connected[^1];
var current = knots[i];
// Check if adjacent and on the same spline as previous
if (!previous.SplineInfo.Equals(current.SplineInfo)
|| previous.KnotIndex + 1 != current.KnotIndex)
{
results.Add(connected.ToArray());
connected.Clear();
}
connected.Add(current);
}
results.Add(connected.ToArray());
// Merge ends if the spline is closed and first and last knots are connected
for (int i = 0; i < results.Count; ++i)
{
var firstKnot = results[i][0];
if (firstKnot.KnotIndex == 0 && firstKnot.SplineInfo.Spline.Closed)
{
// Look for the last knot on the same spline
for (int j = i + 1; j < results.Count; ++j)
{
var lastKnot = results[j][^1];
// Early exit if not on the same spline
if (!lastKnot.SplineInfo.Equals(firstKnot.SplineInfo))
break;
if (lastKnot.KnotIndex == lastKnot.SplineInfo.Spline.Count - 1)
{
// combine both arrays
var a = results[j];
var b = results[i];
var newArray = new SelectableKnot[a.Length + b.Length];
Array.Copy(a, newArray, a.Length);
Array.Copy(b, 0, newArray, a.Length, b.Length);
results[i] = newArray;
results.RemoveAt(j);
break;
}
}
}
}
return results;
}
// Paste will create all new splines in the first active ISplineContainer in the selection.
// Duplicate will try to create new splines in the same container that the knots were copied from.
public static void Paste(string copyPasteBuffer)
{
ISplineContainer target = Selection.GetFiltered<ISplineContainer>(SelectionMode.TopLevel).FirstOrDefault() ??
ObjectFactory.CreateGameObject("New Spline", typeof(SplineContainer)).GetComponent<SplineContainer>();
Paste(copyPasteBuffer, target);
}
public static void Paste(string copyPasteBuffer, ISplineContainer target)
{
if (target == null)
throw new ArgumentNullException(nameof(target));
if (string.IsNullOrEmpty(copyPasteBuffer))
return;
var buffer = new CopyPasteBuffer();
try
{
EditorJsonUtility.FromJsonOverwrite(copyPasteBuffer, buffer);
}
catch (ArgumentException)
{
//If the copy buffer wasn't for a spline copy buffer, we just don't do anything
return;
}
var selection = new List<SelectableKnot>();
var inverse = (target is Component component)
? component.transform.localToWorldMatrix.inverse
: Matrix4x4.identity;
var branches = new List<Spline>(target.Splines);
int splineIndexOffset = branches.Count;
foreach (var serialized in buffer.Splines)
{
var knots = serialized.Knots;
var spline = new Spline(knots.Length);
spline.Closed = serialized.Closed;
var trs = serialized.Transform;
var index = branches.Count;
var info = new SplineInfo(target, index);
branches.Add(spline);
for (int i = 0, c = knots.Length; i < c; ++i)
{
spline.Add(knots[i].Knot.Transform(math.mul(inverse, trs)), knots[i].Mode, knots[i].Tension);
selection.Add(new SelectableKnot(info, i));
}
}
if (target is Object obj)
Undo.RecordObject(obj, "Paste Knots");
target.Splines = branches;
foreach (var link in buffer.Links)
{
var firstIndex = link.Indices[0];
firstIndex.Spline += splineIndexOffset;
for (int i = 1; i < link.Indices.Length; ++i)
{
var indexPair = link.Indices[i];
indexPair.Spline += splineIndexOffset;
target.KnotLinkCollection.Link(firstIndex, indexPair);
}
}
SplineSelection.Clear();
SplineSelection.AddRange(selection);
SceneView.RepaintAll();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 641a050c859644e7804f826b7d4d8c0b
timeCreated: 1656006689

View File

@@ -0,0 +1,41 @@
using UnityEditor.SettingsManagement;
using UnityEngine;
namespace UnityEditor.Splines
{
sealed class PathSettings
{
static Settings s_SettingsInstance;
public static Settings instance
{
get
{
if (s_SettingsInstance == null)
s_SettingsInstance = new Settings(new [] { new UserSettingsRepository() });
return s_SettingsInstance;
}
}
// Register a new SettingsProvider that will scrape the owning assembly for [UserSetting] marked fields.
[SettingsProvider]
static SettingsProvider CreateSettingsProvider()
{
var provider = new UserSettingsProvider("Preferences/Splines",
instance,
new[] { typeof(PathSettings).Assembly });
return provider;
}
}
/// <summary>
/// The wrapper to define user preferences through Settings Manager.
/// </summary>
/// <typeparam name="T"></typeparam>
class Pref<T> : UserSetting<T>
{
public Pref(string key, T value)
: base(PathSettings.instance, key, value, SettingsScope.User) { }
}
}

View File

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

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using Object = UnityEngine.Object;
namespace UnityEditor.Splines
{
[Serializable]
struct SelectableSplineElement : IEquatable<SelectableSplineElement>
{
public Object target;
public int targetIndex;
public int knotIndex;
public int tangentIndex;
public SelectableSplineElement(ISelectableElement element)
{
target = element.SplineInfo.Object;
targetIndex = element.SplineInfo.Index;
knotIndex = element.KnotIndex;
tangentIndex = element is SelectableTangent tangent ? tangent.TangentIndex : -1;
}
public bool Equals(SelectableSplineElement other)
{
return target == other.target && targetIndex == other.targetIndex && knotIndex == other.knotIndex && tangentIndex == other.tangentIndex;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
return obj is SelectableSplineElement other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(target, targetIndex, knotIndex, tangentIndex);
}
}
sealed class SelectionContext : ScriptableSingleton<SelectionContext>
{
public List<SelectableSplineElement> selection = new List<SelectableSplineElement>();
public int version;
}
}

View File

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

View File

@@ -0,0 +1,385 @@
using System;
using Unity.Mathematics;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
/// <summary>
/// An interface that represents a selectable spline element. A selectable spline element can be a knot or a tangent.
/// `ISelectableElement` is used by the selection to get information about the spline, the knot, and the positions of the spline elements.
/// </summary>
public interface ISelectableElement : IEquatable<ISelectableElement>
{
/// <summary>
/// The <see cref="SplineInfo"/> that describes the spline.
/// </summary>
SplineInfo SplineInfo { get; }
/// <summary>
/// The index of the knot in the spline. If the spline element is a tangent, this is the index of the knot
/// that the tangent is attached to.
/// </summary>
int KnotIndex { get; }
/// <summary>
/// The position of the spline element in local space.
/// </summary>
float3 LocalPosition { get; set; }
/// <summary>
/// The position of the spline element in world space.
/// </summary>
float3 Position { get; set; }
/// <summary>
/// Checks if the element is valid. For example, checks if the spline is not null and the index is valid.
/// </summary>
/// <returns>Returns true if all fields from the element have valid values.</returns>
bool IsValid();
}
/// <summary>
/// Implements the <see cref="ISelectableElement"/> interface. SelectableKnot is used by the
/// spline selection and handles to use tools and handles to manipulate spline elements.
/// </summary>
public struct SelectableKnot : ISelectableElement, IEquatable<SelectableKnot>
{
/// <inheritdoc />
public SplineInfo SplineInfo { get; }
/// <inheritdoc />
public int KnotIndex { get; }
/// <summary>
/// Transforms a knot from local space to world space (Read Only).
/// </summary>
internal float4x4 LocalToWorld => SplineInfo.LocalToWorld;
/// <inheritdoc />
public float3 Position
{
get => math.transform(LocalToWorld, LocalPosition);
set => LocalPosition = math.transform(math.inverse(LocalToWorld), value);
}
/// <inheritdoc />
public float3 LocalPosition
{
get => SplineInfo.Spline[KnotIndex].Position;
set
{
var knot = SplineInfo.Spline[KnotIndex];
knot.Position = value;
SplineInfo.Spline[KnotIndex] = knot;
}
}
/// <inheritdoc />
public bool IsValid()
{
return SplineInfo.Spline != null && KnotIndex >= 0 && KnotIndex < SplineInfo.Spline.Count;
}
/// <summary>
/// The rotation of the spline element in world space.
/// </summary>
public quaternion Rotation
{
get => math.mul(new quaternion(LocalToWorld), LocalRotation);
set => LocalRotation = math.mul(math.inverse(new quaternion(LocalToWorld)), value);
}
/// <summary>
/// The rotation of the spline element in local space.
/// </summary>
public quaternion LocalRotation
{
get => SplineInfo.Spline[KnotIndex].Rotation;
set
{
var knot = SplineInfo.Spline[KnotIndex];
knot.Rotation = math.normalize(value);
SplineInfo.Spline[KnotIndex] = knot;
}
}
/// <summary>
/// The <see cref="TangentMode"/> associated with a knot.
/// </summary>
public TangentMode Mode
{
get => SplineInfo.Spline.GetTangentMode(KnotIndex);
set
{
SplineInfo.Spline.SetTangentMode(KnotIndex, value);
SplineSelectionUtility.ValidateTangentSelection(this);
}
}
/// <summary>
/// The tension associated with a knot. `Tension` is only used if the tangent mode is Auto Smooth.
/// </summary>
public float Tension
{
get => SplineInfo.Spline.GetAutoSmoothTension(KnotIndex);
set => SplineInfo.Spline.SetAutoSmoothTension(KnotIndex, value);
}
/// <summary>
/// Sets the tangent mode of the knot.
/// </summary>
/// <param name="mode">The <see cref="TangentMode"/> to apply to the knot.</param>
/// <param name="main">The tangent to use as the main tangent when the tangent is set to the Mirrored or Continuous tangent mode.
/// The main tangent is not modified, but the other tangent attached to that knot is modified to adopt the new tangent mode.</param>
public void SetTangentMode(TangentMode mode, BezierTangent main)
{
var spline = SplineInfo.Spline;
spline.SetTangentMode(KnotIndex, mode, main);
SplineSelectionUtility.ValidateTangentSelection(this);
}
/// <summary>
/// The In tangent associated with the knot. The In tangent defines the curvature of the segment that enters the knot.
/// </summary>
public SelectableTangent TangentIn => new SelectableTangent(SplineInfo, KnotIndex, BezierTangent.In);
/// <summary>
/// The Out tangent associated with the knot. The Out tangent defines the curvature of the segment that exits the knot.
/// </summary>
public SelectableTangent TangentOut => new SelectableTangent(SplineInfo, KnotIndex, BezierTangent.Out);
/// <summary>
/// Creates a <see cref="SelectableKnot"/> from a SplineInfo and a knot index.
/// </summary>
/// <param name="info">The <see cref="SplineInfo"/> associated with the tangent.</param>
/// <param name="index">The index of the knot.</param>
public SelectableKnot(SplineInfo info, int index)
{
this.SplineInfo = info;
this.KnotIndex = index;
}
/// <summary>
/// Creates the BezierKnot representation associated with a SelectableKnot.
/// </summary>
/// <param name="worldSpace">Set to true for the BezierKnot to be in world space, or set to false for the Bezierknot to be in local space.</param>
/// <returns>The <see cref="BezierKnot"/> associated with the knot.</returns>
public BezierKnot GetBezierKnot(bool worldSpace)
{
return worldSpace ? SplineInfo.Spline[KnotIndex].Transform(LocalToWorld) : SplineInfo.Spline[KnotIndex];
}
/// <summary>
/// Checks if two instances of `SplineElement` are equal.
/// </summary>
/// <param name="other">The <see cref="ISelectableElement"/> to compare against.</param>
/// <returns>
/// Returns true when <paramref name="other"/> is a <see cref="SelectableKnot"/> and the values of each instance are identical.
/// </returns>
public bool Equals(ISelectableElement other)
{
if (other is SelectableKnot knot)
return Equals(knot);
return false;
}
/// <summary>
/// Checks if two instances of SelectableKnot are equal.
/// </summary>
/// <param name="other">The <see cref="SelectableKnot"/> to compare against.</param>
/// <returns>
/// Returns true if the values of each instance of `SelectableKnot` are identical.
/// </returns>
public bool Equals(SelectableKnot other)
{
return Equals(SplineInfo.Spline, other.SplineInfo.Spline) && KnotIndex == other.KnotIndex;
}
/// <summary>
/// Checks if two instances of an object are equal.
/// </summary>
/// <param name="obj">The object to compare against.</param>
/// <returns>
/// Returns true if <paramref name="obj"/> is a <see cref="SelectableKnot"/> and its values are identical to the original instance.
/// </returns>
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
return obj is SelectableKnot other && Equals(other);
}
/// <summary>
/// Gets a hash code for this knot.
/// </summary>
/// <returns>
/// A hash code for the <see cref="SelectableKnot"/>.
/// </returns>
public override int GetHashCode()
{
return HashCode.Combine(SplineInfo.Spline, KnotIndex);
}
}
/// <summary>
/// Represents a struct that implements the <see cref="ISelectableElement"/> interface. Spline selection uses
/// `SelectableTangent` and handles to easily manipulate spline elements with tools and handles.
/// </summary>
public struct SelectableTangent : ISelectableElement, IEquatable<SelectableTangent>
{
/// <inheritdoc />
public SplineInfo SplineInfo { get; }
/// <inheritdoc />
public int KnotIndex { get; }
/// <summary>
/// The index of the tangent. A value of 0 represents an In tangent. A value of 1 represents an Out tangent.
/// </summary>
public int TangentIndex { get; }
/// <summary>
/// The knot associated with this tangent.
/// </summary>
public SelectableKnot Owner => new SelectableKnot(SplineInfo, KnotIndex);
/// <summary>
/// The opposite tangent on the knot. If this tangent is the In tangent, then the opposite tangent is the Out tangent. If this tangent is the Out tangent, then the opposite tangent is the In tangent.
/// </summary>
public SelectableTangent OppositeTangent => new SelectableTangent(SplineInfo, KnotIndex, 1 - TangentIndex);
/// <inheritdoc />
public bool IsValid()
{
return SplineInfo.Spline != null
&& KnotIndex >= 0
&& KnotIndex < SplineInfo.Spline.Count
&& TangentIndex >= 0
&& TangentIndex < 2
&& Owner.Mode != TangentMode.Linear
&& Owner.Mode != TangentMode.AutoSmooth;
}
/// <summary>
/// The direction of the tangent in world space.
/// </summary>
public float3 Direction
{
get => MathUtility.MultiplyVector(LocalToWorld, LocalDirection);
set => LocalDirection = MathUtility.MultiplyVector(math.inverse(LocalToWorld), value);
}
/// <summary>
/// The direction of the tangent in local space.
/// </summary>
public float3 LocalDirection
{
get => TangentIndex == (int)BezierTangent.In ? SplineInfo.Spline[KnotIndex].TangentIn : SplineInfo.Spline[KnotIndex].TangentOut;
set
{
var spline = SplineInfo.Spline;
var knot = spline[KnotIndex];
switch (TangentIndex)
{
case (int)BezierTangent.In:
knot.TangentIn = value;
break;
case (int)BezierTangent.Out:
knot.TangentOut = value;
break;
}
spline.SetKnot(KnotIndex, knot, (BezierTangent)TangentIndex);
}
}
/// <summary>
/// Matrix that transforms a tangent point from local space into world space using its associated knot (Read Only).
/// </summary>
internal float4x4 LocalToWorld => math.mul(SplineInfo.LocalToWorld, new float4x4(Owner.LocalRotation, Owner.LocalPosition));
/// <inheritdoc />
public float3 Position
{
get => math.transform(LocalToWorld, LocalPosition);
set => LocalPosition = math.transform(math.inverse(LocalToWorld), value);
}
/// <inheritdoc />
public float3 LocalPosition
{
get => LocalDirection;
set => LocalDirection = value;
}
/// <summary>
/// Creates a new <see cref="SelectableTangent"/> object.
/// </summary>
/// <param name="info">The <see cref="SplineInfo"/> associated with the tangent.</param>
/// <param name="knotIndex">The index of the knot that the tangent is attached to.</param>
/// <param name="tangent">The <see cref="BezierTangent"/> that represents this tangent.</param>
public SelectableTangent(SplineInfo info, int knotIndex, BezierTangent tangent)
: this(info, knotIndex, (int)tangent) { }
/// <summary>
/// Creates a new <see cref="SelectableTangent"/> object.
/// </summary>
/// <param name="info">The <see cref="SplineInfo"/> associated with the tangent.</param>
/// <param name="knotIndex">The index of the knot that the tangent is attached to.</param>
/// <param name="tangentIndex">The index of the tangent. A value of 0 represents an In tangent. A value of 1 represents an Out tangent.</param>
public SelectableTangent(SplineInfo info, int knotIndex, int tangentIndex)
{
SplineInfo = info;
KnotIndex = knotIndex;
TangentIndex = tangentIndex;
}
/// <summary>
/// Checks if two instances of a `SplineElement` are equal.
/// </summary>
/// <param name="other">The <see cref="ISelectableElement"/> to compare against.</param>
/// <returns>
/// Returns true when <paramref name="other"/> is a <see cref="SelectableTangent"/> and the values of each instance are identical.
/// </returns>
public bool Equals(ISelectableElement other)
{
if (other is SelectableTangent tangent)
return Equals(tangent);
return false;
}
/// <summary>
/// Checks if two instances of `SelectableTangent` are equal.
/// </summary>
/// <param name="other">The <see cref="SelectableTangent"/> to compare against.</param>
/// <returns>
/// Returns true if the values of each instance are identical.
/// </returns>
public bool Equals(SelectableTangent other)
{
return Equals(SplineInfo.Spline, other.SplineInfo.Spline) && KnotIndex == other.KnotIndex && TangentIndex == other.TangentIndex;
}
/// <summary>
/// Checks if two objects are equal.
/// </summary>
/// <param name="obj">The object to compare against.</param>
/// <returns>
/// Returns true when <paramref name="obj"/> is a <see cref="SelectableTangent"/> and the values of each instance are identical.
/// </returns>
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
return obj is SelectableTangent other && Equals(other);
}
/// <summary>
/// Gets a hash code for this tangent.
/// </summary>
/// <returns>
/// A hash code for the <see cref="SelectableTangent"/>.
/// </returns>
public override int GetHashCode()
{
return HashCode.Combine(SplineInfo.Spline, KnotIndex, TangentIndex);
}
}
}

View File

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

View File

@@ -0,0 +1,610 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Splines;
using Object = UnityEngine.Object;
namespace UnityEditor.Splines
{
/// <summary>
/// Provides methods to track the selection of spline elements, knots, and tangents.
/// `SplineTools` and `SplineHandles` use `SplineSelection` to manage these elements.
/// </summary>
public static class SplineSelection
{
/// <summary>
/// Action that is called when the element selection changes.
/// </summary>
public static event Action changed;
static readonly HashSet<Object> s_ObjectSet = new HashSet<Object>();
static readonly HashSet<SplineInfo> s_SelectedSplineInfo = new HashSet<SplineInfo>();
static Object[] s_SelectedTargetsBuffer = new Object[0];
static SelectionContext context => SelectionContext.instance;
internal static List<SelectableSplineElement> selection => context.selection;
// Tracks selected splines in the SplineReorderableList
static List<SplineInfo> s_SelectedSplines = new ();
/// <summary>
/// The number of elements in the current selection.
/// </summary>
public static int Count => selection.Count;
static HashSet<SelectableTangent> s_AdjacentTangentCache = new HashSet<SelectableTangent>();
static int s_SelectionVersion;
static SplineSelection()
{
context.version = 0;
Undo.undoRedoPerformed += OnUndoRedoPerformed;
EditorSplineUtility.knotInserted += OnKnotInserted;
EditorSplineUtility.knotRemoved += OnKnotRemoved;
Selection.selectionChanged += OnSelectionChanged;
}
static void OnSelectionChanged()
{
ClearInspectorSelectedSplines();
}
static void OnUndoRedoPerformed()
{
if (context.version != s_SelectionVersion)
{
s_SelectionVersion = context.version;
ClearInspectorSelectedSplines();
NotifySelectionChanged();
}
}
/// <summary>
/// Clears the current selection.
/// </summary>
public static void Clear()
{
if (selection.Count == 0)
return;
IncrementVersion();
ClearNoUndo(true);
}
internal static void ClearNoUndo(bool notify)
{
selection.Clear();
if (notify)
NotifySelectionChanged();
}
/// <summary>
/// Checks if the current selection contains at least one element from the given targeted splines.
/// </summary>
/// <param name="targets">The splines to consider when looking at selected elements.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
/// <returns>Returns true if the current selection contains at least an element of the desired type.</returns>
public static bool HasAny<T>(IReadOnlyList<SplineInfo> targets)
where T : struct, ISelectableElement
{
for (int i = 0; i < Count; ++i)
for (int j = 0; j < targets.Count; ++j)
if (TryGetElement(selection[i], targets[j], out T _))
return true;
return false;
}
/// <summary>
/// Gets the active element of the selection. The active element is generally the last one added to this selection.
/// </summary>
/// <param name="targets">The splines to consider when getting the active element.</param>
/// <returns>The <see cref="ISelectableElement"/> that represents the active knot or tangent. Returns null if no active element is found.</returns>
public static ISelectableElement GetActiveElement(IReadOnlyList<SplineInfo> targets)
{
for (int i = 0; i < Count; ++i)
for (int j = 0; j < targets.Count; ++j)
if (TryGetElement(selection[i], targets[j], out ISelectableElement result))
return result;
return null;
}
/// <summary>
/// Gets all the elements of the current selection, filtered by target splines. Elements are added to the given collection.
/// </summary>
/// <param name="targets">The splines to consider when looking at selected elements.</param>
/// <param name="results">The collection to fill with spline elements from the selection.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
public static void GetElements<T>(IReadOnlyList<SplineInfo> targets, ICollection<T> results)
where T : ISelectableElement
{
results.Clear();
for (int i = 0; i < Count; ++i)
for (int j = 0; j < targets.Count; ++j)
if (TryGetElement(selection[i], targets[j], out T result))
results.Add(result);
}
/// <summary>
/// Gets all the elements of the current selection, from a single spline target. Elements are added to the given collection.
/// </summary>
/// <param name="target">The spline to consider when looking at selected elements.</param>
/// <param name="results">The collection to fill with spline elements from the selection.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
public static void GetElements<T>(SplineInfo target, ICollection<T> results)
where T : ISelectableElement
{
results.Clear();
for (int i = 0; i < Count; ++i)
if (TryGetElement(selection[i], target, out T result))
results.Add(result);
}
static bool TryGetElement<T>(SelectableSplineElement element, SplineInfo splineInfo, out T value)
where T : ISelectableElement
{
if (element.target == splineInfo.Container as Object)
{
if (element.targetIndex == splineInfo.Index)
{
if (element.tangentIndex >= 0)
{
var tangent = new SelectableTangent(splineInfo, element.knotIndex, element.tangentIndex);
if (tangent.IsValid() && tangent is T t)
{
value = t;
return true;
}
value = default;
return false;
}
var knot = new SelectableKnot(splineInfo, element.knotIndex);
if (knot.IsValid() && knot is T k)
{
value = k;
return true;
}
}
}
value = default;
return false;
}
internal static Object[] GetAllSelectedTargets()
{
s_ObjectSet.Clear();
foreach (var element in selection)
{
s_ObjectSet.Add(element.target);
}
Array.Resize(ref s_SelectedTargetsBuffer, s_ObjectSet.Count);
s_ObjectSet.CopyTo(s_SelectedTargetsBuffer);
return s_SelectedTargetsBuffer;
}
/// <summary>
/// Used for selecting splines from the inspector, only internal from now.
/// </summary>
internal static IEnumerable<SplineInfo> SelectedSplines => s_SelectedSplines;
/// <summary>
/// Checks if an element is currently the active one in the selection.
/// </summary>
/// <param name="element">The <see cref="ISelectableElement"/> to test.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
/// <returns>Returns true if the element is the active element, false if it is not.</returns>
public static bool IsActive<T>(T element)
where T : ISelectableElement
{
if (selection.Count == 0)
return false;
return IsEqual(element, selection[0]);
}
static bool IsEqual<T>(T element, SelectableSplineElement selectionData)
where T : ISelectableElement
{
int tangentIndex = element is SelectableTangent tangent ? tangent.TangentIndex : -1;
return element.SplineInfo.Object == selectionData.target
&& element.SplineInfo.Index == selectionData.targetIndex
&& element.KnotIndex == selectionData.knotIndex
&& tangentIndex == selectionData.tangentIndex;
}
/// <summary>
/// Sets the active element of the selection.
/// </summary>
/// <param name="element">The <see cref="ISelectableElement"/> to set as the active element.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
public static void SetActive<T>(T element)
where T : ISelectableElement
{
var index = IndexOf(element);
if (index == 0)
return;
IncrementVersion();
if (index > 0)
selection.RemoveAt(index);
var e = new SelectableSplineElement(element);
selection.Insert(0, e);
if(e.target is Component component)
{
//Set the active unity object so the spline is the first target
Object[] unitySelection = Selection.objects;
var target = component.gameObject;
index = Array.IndexOf(unitySelection, target);
if(index > 0)
{
Object prevObj = unitySelection[0];
unitySelection[0] = unitySelection[index];
unitySelection[index] = prevObj;
Selection.objects = unitySelection;
}
}
NotifySelectionChanged();
}
/// <summary>
/// Sets the selection to the element.
/// </summary>
/// <param name="element">The <see cref="ISelectableElement"/> to set as the selection.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
public static void Set<T>(T element)
where T : ISelectableElement
{
IncrementVersion();
ClearNoUndo(false);
selection.Insert(0, new SelectableSplineElement(element));
NotifySelectionChanged();
}
internal static void Set(IEnumerable<SelectableSplineElement> selection)
{
IncrementVersion();
context.selection.Clear();
context.selection.AddRange(selection);
NotifySelectionChanged();
}
/// <summary>
/// Adds an element to the current selection.
/// </summary>
/// <param name="element">The <see cref="ISelectableElement"/> to add to the selection.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
/// <returns>Returns true if the element was added to the selection, and false if the element is already in the selection.</returns>
public static bool Add<T>(T element)
where T : ISelectableElement
{
if (Contains(element))
return false;
IncrementVersion();
selection.Insert(0, new SelectableSplineElement(element));
NotifySelectionChanged();
return true;
}
/// <summary>
/// Add a set of elements to the current selection.
/// </summary>
/// <param name="elements">The set of <see cref="ISelectableElement"/> to add to the selection.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
public static void AddRange<T>(IEnumerable<T> elements)
where T : ISelectableElement
{
bool changed = false;
foreach (var element in elements)
{
if (!Contains(element))
{
if (!changed)
{
changed = true;
IncrementVersion();
}
selection.Insert(0, new SelectableSplineElement(element));
}
}
if (changed)
NotifySelectionChanged();
}
/// <summary>
/// Remove an element from the current selection.
/// </summary>
/// <param name="element">The <see cref="ISelectableElement"/> to remove from the selection.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
/// <returns>Returns true if the element has been removed from the selection, false otherwise.</returns>
public static bool Remove<T>(T element)
where T : ISelectableElement
{
var index = IndexOf(element);
if (index >= 0)
{
IncrementVersion();
selection.RemoveAt(index);
NotifySelectionChanged();
return true;
}
return false;
}
/// <summary>
/// Remove a set of elements from the current selection.
/// </summary>
/// <param name="elements">The set of <see cref="ISelectableElement"/> to remove from the selection.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
/// <returns>Returns true if at least an element has been removed from the selection, false otherwise.</returns>
public static bool RemoveRange<T>(IReadOnlyList<T> elements)
where T : ISelectableElement
{
bool changed = false;
for (int i = 0; i < elements.Count; ++i)
{
var index = IndexOf(elements[i]);
if (index >= 0)
{
if (!changed)
{
IncrementVersion();
changed = true;
}
selection.RemoveAt(index);
}
}
if (changed)
NotifySelectionChanged();
return changed;
}
static int IndexOf<T>(T element)
where T : ISelectableElement
{
for (int i = 0; i < selection.Count; ++i)
if (IsEqual(element, selection[i]))
return i;
return -1;
}
/// <summary>
/// Checks if the selection contains a knot or a tangent.c'est
/// </summary>
/// <param name="element">The element to verify.</param>
/// <typeparam name="T"><see cref="SelectableKnot"/> or <see cref="SelectableTangent"/>.</typeparam>
/// <returns>Returns true if the element is contained in the current selection, false otherwise.</returns>
public static bool Contains<T>(T element)
where T : ISelectableElement
{
return IndexOf(element) >= 0;
}
// Used when the selection is changed in the tools.
internal static void UpdateObjectSelection(IEnumerable<Object> targets)
{
s_ObjectSet.Clear();
foreach (var target in targets)
if (target != null)
s_ObjectSet.Add(target);
bool changed = false;
for (int i = Count - 1; i >= 0; --i)
{
bool removeElement = false;
if (!EditorSplineUtility.Exists(selection[i].target as ISplineContainer, selection[i].targetIndex))
{
removeElement = true;
}
else if (!s_ObjectSet.Contains(selection[i].target))
{
ClearInspectorSelectedSplines();
removeElement = true;
}
else if(selection[i].tangentIndex > 0)
{
// In the case of a tangent, also check that the tangent is still valid if the spline type
// or tangent mode has been updated
var spline = SplineToolContext.GetSpline(selection[i].target, selection[i].targetIndex);
removeElement = !SplineUtility.AreTangentsModifiable(spline.GetTangentMode(selection[i].knotIndex));
}
if (removeElement)
{
if (!changed)
{
changed = true;
IncrementVersion();
}
if (i < selection.Count)
selection.RemoveAt(i);
}
}
if (changed)
{
RebuildAdjacentCache();
NotifySelectionChanged();
}
}
//Used when inserting new elements in spline
static void OnKnotInserted(SelectableKnot inserted)
{
for (var i = 0; i < selection.Count; ++i)
{
var knot = selection[i];
if (knot.target == inserted.SplineInfo.Object
&& knot.targetIndex == inserted.SplineInfo.Index
&& knot.knotIndex >= inserted.KnotIndex)
{
++knot.knotIndex;
selection[i] = knot;
}
}
RebuildAdjacentCache();
}
//Used when deleting an element in spline
static void OnKnotRemoved(SelectableKnot removed)
{
bool changed = false;
for (var i = selection.Count - 1; i >= 0; --i)
{
var knot = selection[i];
if (knot.target == removed.SplineInfo.Object && knot.targetIndex == removed.SplineInfo.Index)
{
if (knot.knotIndex == removed.KnotIndex)
{
if (!changed)
{
changed = true;
IncrementVersion();
}
selection.RemoveAt(i);
}
else if (knot.knotIndex >= removed.KnotIndex)
{
--knot.knotIndex;
selection[i] = knot;
}
}
}
RebuildAdjacentCache();
if (changed)
NotifySelectionChanged();
}
static void IncrementVersion()
{
Undo.RecordObject(context, "Spline Selection Changed");
++s_SelectionVersion;
++context.version;
}
static void NotifySelectionChanged()
{
RebuildAdjacentCache();
changed?.Invoke();
}
static bool TryGetSplineInfo(SelectableSplineElement element, out SplineInfo splineInfo)
{
//Checking null in case the object was destroyed
if (element.target != null && element.target is ISplineContainer container)
{
splineInfo = new SplineInfo(container, element.targetIndex);
return true;
}
splineInfo = default;
return false;
}
static bool TryCast(SelectableSplineElement element, out SelectableTangent result)
{
if (TryGetSplineInfo(element, out var splineInfo) && element.tangentIndex >= 0)
{
result = new SelectableTangent(splineInfo, element.knotIndex, element.tangentIndex);
return true;
}
result = default;
return false;
}
static bool TryCast(SelectableSplineElement element, out SelectableKnot result)
{
if (TryGetSplineInfo(element, out var splineInfo) && element.tangentIndex < 0)
{
result = new SelectableKnot(splineInfo, element.knotIndex);
return true;
}
result = default;
return false;
}
internal static bool IsSelectedOrAdjacentToSelected(SelectableTangent tangent)
{
return s_AdjacentTangentCache.Contains(tangent);
}
static void RebuildAdjacentCache()
{
s_AdjacentTangentCache.Clear();
foreach(var element in selection)
{
SelectableTangent previousOut, currentIn, currentOut, nextIn;
if(TryCast(element, out SelectableKnot knot))
EditorSplineUtility.GetAdjacentTangents(knot, out previousOut, out currentIn, out currentOut, out nextIn);
else if(TryCast(element, out SelectableTangent tangent))
EditorSplineUtility.GetAdjacentTangents(tangent, out previousOut, out currentIn, out currentOut, out nextIn);
else
continue;
s_AdjacentTangentCache.Add(previousOut);
s_AdjacentTangentCache.Add(currentIn);
s_AdjacentTangentCache.Add(currentOut);
s_AdjacentTangentCache.Add(nextIn);
}
}
internal static void ClearInspectorSelectedSplines()
{
s_SelectedSplines.Clear();
}
internal static bool HasActiveSplineSelection()
{
return s_SelectedSplines.Count > 0;
}
// Inspector spline selection is a one-way operation. It can only be set by the SplineReorderableList. Changes
// to selected splines in the Scene or Hierarchy will only clear the selected inspector splines.
internal static void SetInspectorSelectedSplines(SplineContainer container, IEnumerable<int> selected)
{
s_SelectedSplines.Clear();
foreach (var index in selected)
s_SelectedSplines.Add(new SplineInfo(container, index));
IncrementVersion();
context.selection = selection.Where(x => x.target == container &&
(selected.Contains(x.targetIndex) || container.KnotLinkCollection.TryGetKnotLinks(new SplineKnotIndex(x.targetIndex, x.knotIndex), out _))).ToList();
NotifySelectionChanged();
}
internal static bool Contains(SplineInfo info)
{
return s_SelectedSplines.Contains(info);
}
internal static bool Remove(SplineInfo info)
{
return s_SelectedSplines.Remove(info);
}
}
}

View File

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

View File

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

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:

Some files were not shown because too many files have changed in this diff Show More