first commit
This commit is contained in:
@@ -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")]
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c854ca5f1d7eed04aa92565d35431e96
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e88477d7eedfb4ca8b5d8912575efa76
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7292ebc78c5d44eaa80048f8fd7d5fe6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9103eb7dcd041455886438d75625d19c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b17d23c06da64c139386c53a0b59281
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ae5aa24d97048439e76e9392765c364
|
||||
timeCreated: 1637689699
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c574a2f698921d1458f6dfba4b5e1229
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6042ea3ccf1fb5c45b533a9d67e956b7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 77221e48184d50742984ffe3b4192f2a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 485daebcb9b4a264ba4f878081549056
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b96c3a8372a8f9945bc2e2b906ac79a3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3d68b08563e248f4a02b78fb710d36bf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8bff491709f26ca4d924ad2c5f505fb0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa11edcb768491b4b8b90ccf074e663f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b7822ae247afa5945befac0bc63be0bb
|
||||
timeCreated: 1654094147
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8805f50579982e7458c1b5ecc0119396
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 411641953fc0a2148a0507f5ee6df182
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 641a050c859644e7804f826b7d4d8c0b
|
||||
timeCreated: 1656006689
|
||||
@@ -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) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0834e0204621424449fe7f88d7127f07
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 33dac614537a60e43ac2b6692f9dc39a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78403350ac663ed44a801175a4fe1b8e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7529a2acb193d1946b9ce1ecb41dd4d5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f0575e8c5ac74a399a62fcb72fe081c4
|
||||
timeCreated: 1634919075
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a0357b2fb04a410d8dc62740f4fe4cbb
|
||||
timeCreated: 1618421082
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eaa3cd6309cba3244b2a227f3e8e7eb3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.Splines;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEditor.Splines
|
||||
{
|
||||
sealed class BezierTangentPropertyField<T> : DropdownField
|
||||
where T : ISelectableElement
|
||||
{
|
||||
static readonly SplineGUIUtility.EqualityComparer<T> s_Comparer = (a, b) =>
|
||||
EditorSplineUtility.GetKnot(a).Mode == EditorSplineUtility.GetKnot(b).Mode;
|
||||
|
||||
static readonly List<string> k_Modes = new List<string>{L10n.Tr("Mirrored"), L10n.Tr("Continuous"), L10n.Tr("Broken")};
|
||||
|
||||
static readonly string k_Tooltip = L10n.Tr(
|
||||
"Mirrored Tangents:\nIf Knot or InTangent is selected, OutTangent will be mirrored on InTangent. Else, InTangent will be mirrored on OutTangent.\n" +
|
||||
"Continuous Tangents:\nInTangent and OutTangent are always aligned.\n" +
|
||||
"Broken Tangents:\nInTangent and OutTangent are dissociated.\n"
|
||||
);
|
||||
|
||||
IReadOnlyList<T> m_Elements = new List<T>(0);
|
||||
|
||||
public event Action changed;
|
||||
|
||||
internal BezierTangentPropertyField() : base(k_Modes, 0)
|
||||
{
|
||||
label = L10n.Tr("Bezier");
|
||||
name = "ModesDropdown";
|
||||
tooltip = k_Tooltip;
|
||||
|
||||
this.RegisterValueChangedCallback(OnValueChange);
|
||||
}
|
||||
|
||||
bool ShouldShow(IReadOnlyList<T> targets)
|
||||
{
|
||||
// Don't show if an element in the selection isn't a bezier mode
|
||||
for (int i = 0; i < targets.Count; ++i)
|
||||
{
|
||||
var knot = EditorSplineUtility.GetKnot(targets[i]);
|
||||
if (!SplineUtility.AreTangentsModifiable(knot.Mode))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static TangentMode GetTangentModeFromIndex(int index)
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0: return TangentMode.Mirrored;
|
||||
case 1: return TangentMode.Continuous;
|
||||
case 2: return TangentMode.Broken;
|
||||
default: return TangentMode.Mirrored;
|
||||
}
|
||||
}
|
||||
|
||||
static int GetIndexFromTangentMode(TangentMode mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case TangentMode.Mirrored: return 0;
|
||||
case TangentMode.Continuous: return 1;
|
||||
case TangentMode.Broken: return 2;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(IReadOnlyList<T> targets)
|
||||
{
|
||||
SetEnabled(ShouldShow(targets));
|
||||
|
||||
m_Elements = targets;
|
||||
SetValueWithoutNotify(EditorSplineUtility.GetKnot(targets[0]).Mode);
|
||||
|
||||
showMixedValue = SplineGUIUtility.HasMultipleValues(targets, s_Comparer);
|
||||
}
|
||||
|
||||
public void SetValueWithoutNotify(TangentMode mode)
|
||||
{
|
||||
SetValueWithoutNotify(k_Modes[GetIndexFromTangentMode(mode)]);
|
||||
}
|
||||
|
||||
void OnValueChange(ChangeEvent<string> evt)
|
||||
{
|
||||
var index = k_Modes.IndexOf(evt.newValue);
|
||||
if (index < 0)
|
||||
return;
|
||||
|
||||
showMixedValue = false;
|
||||
var targetMode = GetTangentModeFromIndex(index);
|
||||
|
||||
EditorSplineUtility.RecordSelection(SplineInspectorOverlay.SplineChangeUndoMessage);
|
||||
for (int i = 0; i < m_Elements.Count; ++i)
|
||||
{
|
||||
var knot = EditorSplineUtility.GetKnot(m_Elements[i]);
|
||||
if (m_Elements[i] is SelectableTangent tangent)
|
||||
knot.SetTangentMode(targetMode, (BezierTangent)tangent.TangentIndex);
|
||||
else
|
||||
knot.Mode = targetMode;
|
||||
}
|
||||
|
||||
changed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84ba2b41d2954a2d8adc64619daa77e8
|
||||
timeCreated: 1649959836
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace UnityEditor.Splines
|
||||
{
|
||||
sealed class CommonElementDrawer : ElementDrawer<ISelectableElement>
|
||||
{
|
||||
readonly TangentModePropertyField<ISelectableElement> m_Mode;
|
||||
readonly BezierTangentPropertyField<ISelectableElement> m_BezierMode;
|
||||
|
||||
public CommonElementDrawer()
|
||||
{
|
||||
Add(m_Mode = new TangentModePropertyField<ISelectableElement>());
|
||||
m_Mode.changed += () => { m_BezierMode.Update(targets); };
|
||||
|
||||
Add(m_BezierMode = new BezierTangentPropertyField<ISelectableElement>());
|
||||
m_BezierMode.changed += () => { m_Mode.Update(targets); };
|
||||
}
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
m_Mode.Update(targets);
|
||||
m_BezierMode.Update(targets);
|
||||
}
|
||||
|
||||
public override string GetLabelForTargets()
|
||||
{
|
||||
int knotCount = 0;
|
||||
int tangentCount = 0;
|
||||
for (int i = 0; i < targets.Count; ++i)
|
||||
{
|
||||
if (targets[i] is SelectableKnot)
|
||||
++knotCount;
|
||||
else if (targets[i] is SelectableTangent)
|
||||
++tangentCount;
|
||||
}
|
||||
|
||||
return $"<b>({knotCount}) Knots</b>, <b>({tangentCount}) Tangents</b> selected";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d0dd2407e33307e4386616f5c33463fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.Splines;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEditor.Splines
|
||||
{
|
||||
interface IElementDrawer
|
||||
{
|
||||
bool HasKnot(Spline spline, int index);
|
||||
void PopulateTargets(IReadOnlyList<SplineInfo> splines);
|
||||
void Update();
|
||||
string GetLabelForTargets();
|
||||
}
|
||||
|
||||
abstract class ElementDrawer<T> : VisualElement, IElementDrawer where T : ISelectableElement
|
||||
{
|
||||
public List<T> targets { get; } = new List<T>();
|
||||
public T target => targets[0];
|
||||
|
||||
public virtual void Update() {}
|
||||
public virtual string GetLabelForTargets() => string.Empty;
|
||||
|
||||
public bool HasKnot(Spline spline, int index)
|
||||
{
|
||||
foreach (var t in targets)
|
||||
if (t.SplineInfo.Spline == spline && t.KnotIndex == index)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void PopulateTargets(IReadOnlyList<SplineInfo> splines)
|
||||
{
|
||||
SplineSelection.GetElements(splines, targets);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95245c7aa2899544b8fe0daa4947e0be
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using UnityEngine.Splines;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEditor.Splines
|
||||
{
|
||||
sealed class ElementInspector : VisualElement, IDisposable
|
||||
{
|
||||
public static bool ignoreKnotCallbacks = false;
|
||||
|
||||
static readonly string k_NoSelectionMessage = L10n.Tr("No element selected");
|
||||
|
||||
readonly Label m_KnotIdentifierLabel;
|
||||
readonly Label m_ErrorMessage;
|
||||
readonly SplineActionButtons m_SplineActionButtons;
|
||||
readonly VisualElement m_SplineDrawerRoot;
|
||||
|
||||
IElementDrawer m_ElementDrawer;
|
||||
readonly CommonElementDrawer m_CommonElementDrawer = new CommonElementDrawer();
|
||||
readonly BezierKnotDrawer m_BezierKnotDrawer = new BezierKnotDrawer();
|
||||
readonly TangentDrawer m_TangentDrawer = new TangentDrawer();
|
||||
|
||||
static StyleSheet s_CommonStyleSheet;
|
||||
static StyleSheet s_ThemeStyleSheet;
|
||||
|
||||
public ElementInspector()
|
||||
{
|
||||
if (s_CommonStyleSheet == null)
|
||||
s_CommonStyleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Packages/com.unity.splines/Editor/Stylesheets/SplineInspectorCommon.uss");
|
||||
if (s_ThemeStyleSheet == null)
|
||||
s_ThemeStyleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>($"Packages/com.unity.splines/Editor/Stylesheets/SplineInspector{(EditorGUIUtility.isProSkin ? "Dark" : "Light")}.uss");
|
||||
|
||||
styleSheets.Add(s_CommonStyleSheet);
|
||||
styleSheets.Add(s_ThemeStyleSheet);
|
||||
|
||||
Add(m_KnotIdentifierLabel = new Label());
|
||||
m_KnotIdentifierLabel.style.height = 24;
|
||||
m_KnotIdentifierLabel.style.unityTextAlign = TextAnchor.MiddleLeft;
|
||||
|
||||
Add(m_ErrorMessage = new Label { name = "ErrorMessage"});
|
||||
Add(m_SplineDrawerRoot = new VisualElement());
|
||||
Add(m_SplineActionButtons = new SplineActionButtons());
|
||||
|
||||
Spline.Changed += OnKnotModified;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Spline.Changed -= OnKnotModified;
|
||||
}
|
||||
|
||||
void OnKnotModified(Spline spline, int index, SplineModification modification)
|
||||
{
|
||||
if (modification == SplineModification.KnotModified && !ignoreKnotCallbacks && m_ElementDrawer != null && m_ElementDrawer.HasKnot(spline, index))
|
||||
m_ElementDrawer.Update();
|
||||
}
|
||||
|
||||
public void UpdateSelection(IReadOnlyList<SplineInfo> selectedSplines)
|
||||
{
|
||||
UpdateDrawerForElements(selectedSplines);
|
||||
|
||||
if (SplineSelection.Count < 1 || m_ElementDrawer == null)
|
||||
{
|
||||
ShowErrorMessage(k_NoSelectionMessage);
|
||||
m_KnotIdentifierLabel.style.display = DisplayStyle.None;
|
||||
m_SplineActionButtons.style.display = DisplayStyle.None;
|
||||
}
|
||||
else
|
||||
{
|
||||
HideErrorMessage();
|
||||
m_ElementDrawer.PopulateTargets(selectedSplines);
|
||||
m_ElementDrawer.Update();
|
||||
m_KnotIdentifierLabel.text = m_ElementDrawer.GetLabelForTargets();
|
||||
m_KnotIdentifierLabel.style.display = DisplayStyle.Flex;
|
||||
m_SplineActionButtons.style.display = DisplayStyle.Flex;
|
||||
}
|
||||
|
||||
m_SplineActionButtons.RefreshSelection(selectedSplines);
|
||||
}
|
||||
|
||||
void UpdateDrawerForElements(IReadOnlyList<SplineInfo> selectedSplines)
|
||||
{
|
||||
bool hasKnot = SplineSelection.HasAny<SelectableKnot>(selectedSplines);
|
||||
bool hasTangent = SplineSelection.HasAny<SelectableTangent>(selectedSplines);
|
||||
|
||||
IElementDrawer targetDrawer;
|
||||
if (hasKnot && hasTangent)
|
||||
targetDrawer = m_CommonElementDrawer;
|
||||
else if (hasKnot)
|
||||
targetDrawer = m_BezierKnotDrawer;
|
||||
else if (hasTangent)
|
||||
targetDrawer = m_TangentDrawer;
|
||||
else
|
||||
targetDrawer = null;
|
||||
|
||||
if (targetDrawer == m_ElementDrawer)
|
||||
return;
|
||||
|
||||
m_SplineDrawerRoot.Clear();
|
||||
if (targetDrawer != null)
|
||||
m_SplineDrawerRoot.Add((VisualElement) targetDrawer);
|
||||
|
||||
m_ElementDrawer = targetDrawer;
|
||||
}
|
||||
|
||||
void ShowErrorMessage(string error)
|
||||
{
|
||||
m_ErrorMessage.style.display = DisplayStyle.Flex;
|
||||
m_ErrorMessage.text = error;
|
||||
}
|
||||
|
||||
void HideErrorMessage()
|
||||
{
|
||||
m_ErrorMessage.style.display = DisplayStyle.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5d8a953f9ef52441bd58f4c1e5853a0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Splines;
|
||||
|
||||
namespace UnityEditor.Splines
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a property drawer for <see cref="EmbeddedSplineData"/> types.
|
||||
/// </summary>
|
||||
/// <seealso cref="EmbeddedSplineDataFieldsAttribute"/>
|
||||
[CustomPropertyDrawer(typeof(EmbeddedSplineData))]
|
||||
[CustomPropertyDrawer(typeof(EmbeddedSplineDataFieldsAttribute))]
|
||||
public class EmbeddedSplineDataPropertyDrawer : PropertyDrawer
|
||||
{
|
||||
bool m_AttemptedFindSplineContainer;
|
||||
static readonly string k_SplineDataKeyContent = "Key";
|
||||
|
||||
static Rect ReserveLine(ref Rect rect, int lines = 1)
|
||||
{
|
||||
var ret = SplineGUIUtility.ReserveSpace(EditorGUIUtility.singleLineHeight * lines, ref rect);
|
||||
rect.y += EditorGUIUtility.standardVerticalSpacing * lines;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int GetSetBitCount(EmbeddedSplineDataField fields)
|
||||
{
|
||||
int c = 0, e = (int) fields;
|
||||
for(int i = 0; i < 4; ++i)
|
||||
if ((e & (1 << i)) != 0)
|
||||
++c;
|
||||
return c;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the height of a SerializedProperty in pixels.
|
||||
/// </summary>
|
||||
/// <param name="property">The SerializedProperty to calculate height for.</param>
|
||||
/// <param name="label">The label of the SerializedProperty.</param>
|
||||
/// <returns>The height of a SerializedProperty in pixels.</returns>
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
var flags = attribute is EmbeddedSplineDataFieldsAttribute attrib
|
||||
? attrib.Fields
|
||||
: EmbeddedSplineDataField.All;
|
||||
|
||||
if (!property.isExpanded)
|
||||
return EditorGUIUtility.singleLineHeight;
|
||||
|
||||
// reserve one line for foldout
|
||||
float height = EditorGUIUtility.singleLineHeight;
|
||||
height += (EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing) * GetSetBitCount(flags);
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an interface for a SerializedProperty with an <see cref="EmbeddedSplineData"/> type.
|
||||
/// </summary>
|
||||
/// <param name="position">Rectangle on the screen to use for the property GUI.</param>
|
||||
/// <param name="property">The SerializedProperty to make the custom GUI for.</param>
|
||||
/// <param name="label">The label of this property.</param>
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
var flags = attribute is EmbeddedSplineDataFieldsAttribute filter
|
||||
? filter.Fields
|
||||
: EmbeddedSplineDataField.All;
|
||||
var fields = GetSetBitCount(flags);
|
||||
|
||||
var container = property.FindPropertyRelative("m_Container");
|
||||
var index = property.FindPropertyRelative("m_SplineIndex");
|
||||
var key = property.FindPropertyRelative("m_Key");
|
||||
var type = property.FindPropertyRelative("m_Type");
|
||||
|
||||
if (fields > 1)
|
||||
{
|
||||
property.isExpanded = EditorGUI.Foldout(ReserveLine(ref position),
|
||||
property.isExpanded,
|
||||
label?.text ?? property.displayName);
|
||||
|
||||
if (!property.isExpanded)
|
||||
return;
|
||||
}
|
||||
|
||||
label = fields == 1 ? label : null;
|
||||
|
||||
// don't create key in property editor
|
||||
// don't copy key values to empty targets
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
// only attempt to reconcile the SplineContainer value once per lifetime of drawer. otherwise you get some
|
||||
// odd behaviour when trying to delete or replace the value in the inspector.
|
||||
if (!m_AttemptedFindSplineContainer
|
||||
&& container.objectReferenceValue == null
|
||||
&& property.serializedObject.targetObject is Component cmp
|
||||
&& cmp.TryGetComponent<SplineContainer>(out var spcnt))
|
||||
{
|
||||
container.objectReferenceValue = spcnt;
|
||||
GUI.changed = true;
|
||||
}
|
||||
|
||||
m_AttemptedFindSplineContainer = true;
|
||||
|
||||
if((flags & EmbeddedSplineDataField.Container) == EmbeddedSplineDataField.Container)
|
||||
EditorGUI.PropertyField(ReserveLine(ref position), container, label);
|
||||
|
||||
if (!(container?.objectReferenceValue is SplineContainer component))
|
||||
component = null;
|
||||
|
||||
if ((flags & EmbeddedSplineDataField.SplineIndex) == EmbeddedSplineDataField.SplineIndex)
|
||||
SplineGUI.SplineIndexField(ReserveLine(ref position), index, label, component);
|
||||
|
||||
if ((flags & EmbeddedSplineDataField.Key) == EmbeddedSplineDataField.Key)
|
||||
{
|
||||
string[] keys = component == null || index.intValue < 0 || index.intValue >= component.Splines.Count
|
||||
? Array.Empty<string>()
|
||||
: component[index.intValue].GetSplineDataKeys((EmbeddedSplineDataType) type.enumValueIndex).ToArray();
|
||||
var i = Array.IndexOf(keys, key.stringValue);
|
||||
EditorGUI.BeginChangeCheck();
|
||||
i = EditorGUI.Popup(ReserveLine(ref position), label?.text ?? k_SplineDataKeyContent, i, keys);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
key.stringValue = keys[i];
|
||||
}
|
||||
|
||||
if((flags & EmbeddedSplineDataField.Type) == EmbeddedSplineDataField.Type)
|
||||
EditorGUI.PropertyField(ReserveLine(ref position), type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44f1bb87db78405e95edea0e310287b2
|
||||
timeCreated: 1671225065
|
||||
@@ -0,0 +1,206 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Splines;
|
||||
|
||||
namespace UnityEditor.Splines
|
||||
{
|
||||
[CustomPropertyDrawer(typeof(TangentMode))]
|
||||
class TangentModePropertyDrawer : PropertyDrawer
|
||||
{
|
||||
static readonly GUIContent[] k_TangentModeLabels = new[]
|
||||
{
|
||||
new GUIContent("Auto", "The path from this knot to the preceding and following knots is a bezier curve that is automatically calculated from the neighboring knot positions."),
|
||||
new GUIContent("Linear", "The path from this knot to the preceding and following knots is a straight line."),
|
||||
new GUIContent("Bezier", "The path from this knot to the preceding and following knots is a bezier curve with manually defined tangents."),
|
||||
};
|
||||
|
||||
static readonly string[] k_BezierModeLabels = new[]
|
||||
{
|
||||
"Mirrored",
|
||||
"Continuous",
|
||||
"Broken"
|
||||
};
|
||||
|
||||
public static float GetPropertyHeight() => SplineGUIUtility.lineHeight * 2 + 2;
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) => GetPropertyHeight();
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
int mode = property.enumValueIndex;
|
||||
int type = System.Math.Min(mode, (int) TangentMode.Mirrored);
|
||||
int bezier = System.Math.Max(mode, (int) TangentMode.Mirrored) - (int)TangentMode.Mirrored;
|
||||
bool mixed = EditorGUI.showMixedValue;
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
EditorGUI.showMixedValue = property.hasMultipleDifferentValues;
|
||||
type = GUI.Toolbar(EditorGUI.IndentedRect(SplineGUIUtility.ReserveSpace(SplineGUIUtility.lineHeight, ref position)),
|
||||
type,
|
||||
k_TangentModeLabels);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
// Continuous mode should be prefered instead of Mirrored when switching from AutoSmooth to Bezier
|
||||
// as Centripetal Catmull-Rom's tangents are not always of equal length.
|
||||
if (property.enumValueIndex == (int)TangentMode.AutoSmooth && type == (int)TangentMode.Mirrored)
|
||||
type = (int)TangentMode.Continuous;
|
||||
|
||||
property.enumValueIndex = type;
|
||||
}
|
||||
|
||||
position.y += 2;
|
||||
position.height = SplineGUIUtility.lineHeight;
|
||||
|
||||
EditorGUI.BeginDisabledGroup(mode < (int) TangentMode.Mirrored);
|
||||
EditorGUI.BeginChangeCheck();
|
||||
EditorGUI.showMixedValue = property.hasMultipleDifferentValues;
|
||||
bezier = EditorGUI.Popup(position, "Bezier", bezier, k_BezierModeLabels);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
property.enumValueIndex = bezier + (int)TangentMode.Mirrored;
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
EditorGUI.showMixedValue = mixed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specialized UI for drawing a knot property drawer with additional data (ex, TangentMode from Spline.MetaData).
|
||||
/// Additionally supports inline fields a little longer than the regular inspector wide mode would allow.
|
||||
/// </summary>
|
||||
static class KnotPropertyDrawerUI
|
||||
{
|
||||
static readonly GUIContent k_Position = EditorGUIUtility.TrTextContent("Position");
|
||||
static readonly GUIContent k_Rotation = EditorGUIUtility.TrTextContent("Rotation");
|
||||
static readonly GUIContent k_TangentLengthContent = EditorGUIUtility.TrTextContent("Tangent Length");
|
||||
static readonly GUIContent k_TangentLengthContentIn = EditorGUIUtility.TrTextContent("Tangent In Length");
|
||||
static readonly GUIContent k_TangentLengthContentOut = EditorGUIUtility.TrTextContent("Tangent Out Length");
|
||||
|
||||
const float k_IndentPad = 13f; // kIndentPerLevel - margin (probably)
|
||||
const int k_MinWideModeWidth = 230;
|
||||
const int k_WideModeInputFieldWidth = 212;
|
||||
|
||||
static bool CanForceWideMode() => EditorGUIUtility.currentViewWidth > k_MinWideModeWidth;
|
||||
|
||||
public static float GetPropertyHeight(SerializedProperty knot, SerializedProperty meta, GUIContent _)
|
||||
{
|
||||
// title
|
||||
float height = SplineGUIUtility.lineHeight;
|
||||
// position, rotation
|
||||
height += SplineGUIUtility.lineHeight * (CanForceWideMode() ? 2 : 4);
|
||||
// 1. { linear, auto, bezier }
|
||||
// 2. { broken, continuous, mirrored }
|
||||
height += meta == null ? 0 : TangentModePropertyDrawer.GetPropertyHeight();
|
||||
// 3. (optional) tangent in
|
||||
// 4. (optional) tangent out
|
||||
height += TangentGetPropertyHeight(meta);
|
||||
|
||||
return knot.isExpanded ? height : SplineGUIUtility.lineHeight;
|
||||
}
|
||||
|
||||
public static float TangentGetPropertyHeight(SerializedProperty meta)
|
||||
{
|
||||
var prop = meta?.FindPropertyRelative("Mode");
|
||||
var mode = meta == null ? TangentMode.Broken : (TangentMode) prop.enumValueIndex;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case TangentMode.AutoSmooth:
|
||||
case TangentMode.Linear:
|
||||
return 0;
|
||||
|
||||
case TangentMode.Mirrored:
|
||||
return SplineGUIUtility.lineHeight;
|
||||
|
||||
case TangentMode.Continuous:
|
||||
return SplineGUIUtility.lineHeight * 2;
|
||||
|
||||
case TangentMode.Broken:
|
||||
default:
|
||||
return SplineGUIUtility.lineHeight * (CanForceWideMode() ? 2 : 4);
|
||||
}
|
||||
}
|
||||
|
||||
public static void TangentOnGUI(ref Rect rect,
|
||||
SerializedProperty tangentIn,
|
||||
SerializedProperty tangentOut,
|
||||
TangentMode mode)
|
||||
{
|
||||
if (mode == TangentMode.Broken)
|
||||
{
|
||||
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpaceForLine(ref rect), tangentIn);
|
||||
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpaceForLine(ref rect), tangentOut);
|
||||
return;
|
||||
}
|
||||
|
||||
// tangents are not serialized as vec3, they are a generic type
|
||||
var tin = tangentIn.FindPropertyRelative("z");
|
||||
var tout = tangentOut.FindPropertyRelative("z");
|
||||
|
||||
if (mode == TangentMode.Mirrored)
|
||||
{
|
||||
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpace(SplineGUIUtility.lineHeight, ref rect), tout, k_TangentLengthContent);
|
||||
tin.floatValue = -tout.floatValue;
|
||||
}
|
||||
else if (mode == TangentMode.Continuous)
|
||||
{
|
||||
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpace(SplineGUIUtility.lineHeight, ref rect), tin, k_TangentLengthContentIn);
|
||||
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpace(SplineGUIUtility.lineHeight, ref rect), tout, k_TangentLengthContentOut);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool OnGUI(Rect rect, SerializedProperty knot, SerializedProperty meta, GUIContent label)
|
||||
{
|
||||
bool wideMode = EditorGUIUtility.wideMode;
|
||||
|
||||
if (!wideMode && CanForceWideMode())
|
||||
{
|
||||
EditorGUIUtility.wideMode = true;
|
||||
EditorGUIUtility.labelWidth = EditorGUIUtility.currentViewWidth - k_WideModeInputFieldWidth;
|
||||
}
|
||||
else
|
||||
EditorGUIUtility.labelWidth = 0;
|
||||
|
||||
var titleRect = SplineGUIUtility.ReserveSpace(SplineGUIUtility.lineHeight, ref rect);
|
||||
titleRect.width = EditorGUIUtility.labelWidth;
|
||||
knot.isExpanded = EditorGUI.Foldout(titleRect, knot.isExpanded, label);
|
||||
var position = knot.FindPropertyRelative("Position");
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
if (knot.isExpanded)
|
||||
{
|
||||
var mode = meta?.FindPropertyRelative("Mode");
|
||||
bool modeAllowsRotationAndTangent = mode?.enumValueIndex > (int) TangentMode.Linear;
|
||||
var tangentMode = mode != null ? (TangentMode) mode.enumValueIndex : TangentMode.Broken;
|
||||
|
||||
var rotation = knot.FindPropertyRelative("Rotation");
|
||||
var tangentIn = knot.FindPropertyRelative("TangentIn");
|
||||
var tangentOut = knot.FindPropertyRelative("TangentOut");
|
||||
|
||||
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpaceForLine(ref rect), position, k_Position);
|
||||
EditorGUI.BeginDisabledGroup(!modeAllowsRotationAndTangent);
|
||||
SplineGUILayout.QuaternionField(SplineGUIUtility.ReserveSpaceForLine(ref rect), k_Rotation, rotation);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
if (meta != null)
|
||||
{
|
||||
EditorGUI.PropertyField(rect, mode);
|
||||
rect.y += TangentModePropertyDrawer.GetPropertyHeight();
|
||||
}
|
||||
|
||||
EditorGUI.BeginDisabledGroup(!modeAllowsRotationAndTangent);
|
||||
TangentOnGUI(ref rect, tangentIn, tangentOut, tangentMode);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
}
|
||||
// When in wide mode, show the position field inline with the knot title if not expanded
|
||||
else if (EditorGUIUtility.wideMode)
|
||||
{
|
||||
var inlinePositionRect = titleRect;
|
||||
inlinePositionRect.x += titleRect.width - k_IndentPad * EditorGUI.indentLevel;
|
||||
inlinePositionRect.width = rect.width - (titleRect.width - k_IndentPad * EditorGUI.indentLevel);
|
||||
EditorGUI.PropertyField(inlinePositionRect, position, GUIContent.none);
|
||||
}
|
||||
|
||||
EditorGUIUtility.wideMode = wideMode;
|
||||
EditorGUIUtility.labelWidth = 0;
|
||||
return EditorGUI.EndChangeCheck();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f3e99075e258cf04898636e5e72ec3e5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,304 @@
|
||||
using UnityEditorInternal;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Splines;
|
||||
|
||||
namespace UnityEditor.Splines
|
||||
{
|
||||
class KnotReorderableList : ReorderableList
|
||||
{
|
||||
ISplineContainer m_Container;
|
||||
int m_ContainerIndex;
|
||||
Spline m_Spline;
|
||||
SerializedProperty m_KnotElement, m_MetaElement;
|
||||
SerializedObject serializedObject => serializedProperty.serializedObject;
|
||||
|
||||
static Dictionary<int, KnotReorderableList> s_Pool = new();
|
||||
|
||||
static KnotReorderableList()
|
||||
{
|
||||
Selection.selectionChanged += ClearPool;
|
||||
SplineSelection.changed += SyncKnotSelection;
|
||||
}
|
||||
|
||||
static int GetPropertyHash(SerializedProperty prop)
|
||||
{
|
||||
if(prop.serializedObject.targetObject == null)
|
||||
return 0;
|
||||
|
||||
unchecked
|
||||
{
|
||||
int hash = prop.serializedObject.targetObject.GetInstanceID();
|
||||
hash = hash * 31 * prop.propertyPath.GetHashCode();
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
static void ClearPool() => s_Pool.Clear();
|
||||
|
||||
static void SyncKnotSelection()
|
||||
{
|
||||
foreach (var kvp in s_Pool)
|
||||
kvp.Value.SyncSelection();
|
||||
|
||||
// InspectorWindow is private for some reason.
|
||||
foreach (var win in Resources.FindObjectsOfTypeAll<EditorWindow>())
|
||||
if (win.GetType().Name.Contains("Inspector"))
|
||||
win.Repaint();
|
||||
}
|
||||
|
||||
public static KnotReorderableList Get(SerializedProperty splineProperty)
|
||||
{
|
||||
int hash = GetPropertyHash(splineProperty);
|
||||
if (!s_Pool.TryGetValue(hash, out var list))
|
||||
s_Pool.Add(hash, list = new KnotReorderableList(splineProperty));
|
||||
list.Init(splineProperty);
|
||||
return list;
|
||||
}
|
||||
|
||||
public KnotReorderableList(SerializedProperty splineProperty)
|
||||
: base(splineProperty.serializedObject, splineProperty.FindPropertyRelative("m_Knots"), true, false, true, true)
|
||||
{
|
||||
Init(splineProperty);
|
||||
multiSelect = true;
|
||||
elementHeightCallback = GetElementHeight;
|
||||
drawElementCallback = OnDrawElement;
|
||||
onReorderCallbackWithDetails = OnReorder;
|
||||
onSelectCallback = OnSelect;
|
||||
onAddCallback = OnAdd;
|
||||
onRemoveCallback = OnRemove;
|
||||
SyncSelection();
|
||||
}
|
||||
|
||||
void Init(SerializedProperty splineProperty)
|
||||
{
|
||||
m_KnotElement = splineProperty.FindPropertyRelative("m_Knots");
|
||||
serializedProperty = m_KnotElement;
|
||||
m_MetaElement = splineProperty.FindPropertyRelative("m_MetaData");
|
||||
|
||||
// only set the ISplineContainer if we are able to determine the correct index into the splines array
|
||||
if (serializedObject.targetObject is ISplineContainer container)
|
||||
{
|
||||
// make sure that this correctly handles the case where ISplineContainer does not serialize an array.
|
||||
// in these cases we'll assert that there is only one spline. if that isn't the case we can't reasonably
|
||||
// handle it so just screw it and give up; the UI can accomodate the case where no container is present.
|
||||
if (SerializedPropertyUtility.TryGetSplineIndex(splineProperty, out m_ContainerIndex))
|
||||
{
|
||||
m_Container = container;
|
||||
|
||||
if (container.Splines.Count > 0)
|
||||
{
|
||||
if (m_Container.Splines.Count < m_ContainerIndex)
|
||||
m_ContainerIndex = m_Container.Splines.Count - 1;
|
||||
|
||||
m_Spline = m_Container.Splines[m_ContainerIndex];
|
||||
}
|
||||
}
|
||||
else if (container.Splines.Count == 1)
|
||||
{
|
||||
m_Container = container;
|
||||
m_Spline = m_Container.Splines[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SerializedProperty GetMetaPropertySafe(int i)
|
||||
{
|
||||
while(m_MetaElement.arraySize < m_KnotElement.arraySize)
|
||||
m_MetaElement.InsertArrayElementAtIndex(m_MetaElement.arraySize);
|
||||
return m_MetaElement.GetArrayElementAtIndex(i);
|
||||
}
|
||||
|
||||
float GetElementHeight(int i) => KnotPropertyDrawerUI.GetPropertyHeight(
|
||||
m_KnotElement.GetArrayElementAtIndex(i),
|
||||
GetMetaPropertySafe(i), GUIContent.none);
|
||||
|
||||
void OnDrawElement(Rect position, int i, bool isActive, bool isFocused)
|
||||
{
|
||||
var knot = m_KnotElement.GetArrayElementAtIndex(i);
|
||||
var meta = GetMetaPropertySafe(i);
|
||||
|
||||
bool guiChanged = false;
|
||||
|
||||
// For reasons unknown, a nested reorderable list requires indent to be incremented to draw elements
|
||||
// with the correct offset. As a hack, we do so when a container is present since we know it will be nested
|
||||
// within a spline reorderable list.
|
||||
if (m_Container != null)
|
||||
{
|
||||
++EditorGUI.indentLevel;
|
||||
guiChanged = KnotPropertyDrawerUI.OnGUI(position, knot, meta, new GUIContent($"Knot [{i}]"));
|
||||
--EditorGUI.indentLevel;
|
||||
}
|
||||
else
|
||||
{
|
||||
guiChanged = KnotPropertyDrawerUI.OnGUI(position, knot, meta, new GUIContent($"Knot [{i}]"));
|
||||
}
|
||||
|
||||
if (guiChanged && m_Spline != null)
|
||||
{
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
m_Spline.EnforceTangentModeNoNotify(m_Spline.PreviousIndex(i));
|
||||
m_Spline.EnforceTangentModeNoNotify(i);
|
||||
m_Spline.EnforceTangentModeNoNotify(m_Spline.NextIndex(i));
|
||||
m_Spline.SetDirty(SplineModification.KnotModified, i);
|
||||
|
||||
// delay repaint because SplineCacheUtility is only clearing it's cache on Spline.afterSplineWasModified
|
||||
EditorApplication.delayCall += SceneView.RepaintAll;
|
||||
}
|
||||
}
|
||||
|
||||
static void EnforceTangentModeWithNeighbors(Spline spline, int index)
|
||||
{
|
||||
if (spline == null)
|
||||
return;
|
||||
int p = spline.PreviousIndex(index), n = spline.NextIndex(index);
|
||||
spline.EnforceTangentModeNoNotify(index);
|
||||
if(p != index) spline.EnforceTangentModeNoNotify(p);
|
||||
if(n != index) spline.EnforceTangentModeNoNotify(n);
|
||||
}
|
||||
|
||||
void OnReorder(ReorderableList reorderableList, int srcIndex, int dstIndex)
|
||||
{
|
||||
m_MetaElement.MoveArrayElement(srcIndex, dstIndex);
|
||||
|
||||
if (m_Container != null)
|
||||
{
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
m_Container.KnotLinkCollection.KnotIndexChanged(m_ContainerIndex, srcIndex, dstIndex);
|
||||
serializedObject.Update();
|
||||
}
|
||||
|
||||
EnforceTangentModeWithNeighbors(m_Spline, srcIndex);
|
||||
EnforceTangentModeWithNeighbors(m_Spline, dstIndex);
|
||||
m_Spline?.SetDirty(SplineModification.KnotReordered, dstIndex);
|
||||
}
|
||||
|
||||
void OnSelect(ReorderableList reorderableList)
|
||||
{
|
||||
if (m_Container != null)
|
||||
{
|
||||
|
||||
SplineSelection.ClearInspectorSelectedSplines();
|
||||
|
||||
var selected = selectedIndices.Select(x => new SelectableSplineElement(
|
||||
new SelectableKnot(new SplineInfo(m_Container, m_ContainerIndex), x))).ToList();
|
||||
|
||||
var evt = Event.current;
|
||||
|
||||
// At the time of this callback, selectedIndices is already set with the correct selection accounting
|
||||
// for shift, ctrl and command. If any modifiers where present, we need to _not_ clear the selection
|
||||
// on other splines but completely replace the selection on this spline.
|
||||
if (evt.modifiers == EventModifiers.Command
|
||||
|| evt.modifiers == EventModifiers.Shift
|
||||
|| evt.modifiers == EventModifiers.Control)
|
||||
{
|
||||
selected.AddRange(SplineSelection.selection.Where(x =>
|
||||
!ReferenceEquals(x.target, m_Container) || x.targetIndex != m_ContainerIndex));
|
||||
}
|
||||
|
||||
SplineSelection.Set(selected);
|
||||
SceneView.RepaintAll();
|
||||
}
|
||||
}
|
||||
|
||||
void OnAdd(ReorderableList _)
|
||||
{
|
||||
if (m_Container != null)
|
||||
{
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
var selectedIndex = index;
|
||||
var info = new SplineInfo(m_Container, m_ContainerIndex);
|
||||
EditorSplineUtility.RecordObject(info, "Add Knot");
|
||||
|
||||
if (selectedIndex < count - 1)
|
||||
{
|
||||
var knot = EditorSplineUtility.InsertKnot(info, selectedIndex + 1, 0.5f);
|
||||
|
||||
SplineSelection.Set(knot);
|
||||
|
||||
index = selectedIndex + 1;
|
||||
Select(selectedIndex + 1);
|
||||
}
|
||||
else // last element from the list
|
||||
{
|
||||
var knot = new SelectableKnot(info, selectedIndex);
|
||||
if (knot.IsValid())
|
||||
{
|
||||
EditorSplineUtility.AddKnotToTheEnd(
|
||||
info,
|
||||
knot.Position + 3f * knot.TangentOut.Direction,
|
||||
math.rotate(knot.LocalToWorld, math.up()),
|
||||
knot.TangentOut.Direction);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorSplineUtility.AddKnotToTheEnd(
|
||||
info,
|
||||
info.Transform.position,
|
||||
math.up(),
|
||||
math.forward());
|
||||
}
|
||||
|
||||
index = count;
|
||||
Select(count);
|
||||
|
||||
}
|
||||
|
||||
serializedObject.Update();
|
||||
}
|
||||
else // if the Spline is not in a ISplineContainer, make default reorderable list
|
||||
{
|
||||
defaultBehaviours.DoAddButton(this);
|
||||
m_MetaElement.InsertArrayElementAtIndex(m_MetaElement.arraySize);
|
||||
}
|
||||
}
|
||||
|
||||
void OnRemove(ReorderableList _)
|
||||
{
|
||||
var toRemove = new List<int>(selectedIndices);
|
||||
toRemove.Sort();
|
||||
|
||||
// Mimic behaviour of a list inspector - if nothing's explicitly selected, remove active or last knot.
|
||||
if (toRemove.Count == 0 && m_Spline.Count > 0)
|
||||
toRemove.Add(index);
|
||||
|
||||
if (m_Container != null)
|
||||
{
|
||||
var info = new SplineInfo(m_Container, m_ContainerIndex);
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
Undo.RecordObject(serializedObject.targetObject, "Remove Knot");
|
||||
|
||||
for (int i = toRemove.Count - 1; i >= 0; --i)
|
||||
EditorSplineUtility.RemoveKnot(new SelectableKnot(info, toRemove[i]));
|
||||
|
||||
SplineSelection.Clear();
|
||||
ClearSelection();
|
||||
|
||||
serializedObject.Update();
|
||||
}
|
||||
else
|
||||
{
|
||||
defaultBehaviours.DoRemoveButton(this);
|
||||
|
||||
for (int i = toRemove.Count - 1; i >= 0; --i)
|
||||
m_MetaElement.DeleteArrayElementAtIndex(toRemove[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public void SyncSelection()
|
||||
{
|
||||
ClearSelection();
|
||||
|
||||
if (m_Container == null)
|
||||
return;
|
||||
|
||||
foreach (var i in SplineSelection.selection
|
||||
.Where(x => ReferenceEquals(x.target, m_Container) && x.targetIndex == m_ContainerIndex && x.tangentIndex < 0)
|
||||
.Select(y => y.knotIndex))
|
||||
Select(i, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 30dcd166c0e84ff5bf290b88ce72bdda
|
||||
timeCreated: 1669232635
|
||||
@@ -0,0 +1,24 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEditor.Splines
|
||||
{
|
||||
sealed class ReadOnlyField : BaseField<string>
|
||||
{
|
||||
readonly Label m_IndexField;
|
||||
|
||||
public ReadOnlyField(string label) : base(label, new Label() { name = "ReadOnlyValue" })
|
||||
{
|
||||
style.flexDirection = FlexDirection.Row;
|
||||
|
||||
m_IndexField = this.Q<Label>("ReadOnlyValue");
|
||||
m_IndexField.text = value;
|
||||
m_IndexField.style.unityTextAlign = TextAnchor.MiddleLeft;
|
||||
}
|
||||
|
||||
public override void SetValueWithoutNotify(string newValue)
|
||||
{
|
||||
m_IndexField.text = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 37aa8727811c9e04785d9c0e4cafc5d3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,137 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEngine.Splines;
|
||||
|
||||
namespace UnityEditor.Splines
|
||||
{
|
||||
sealed class SplineActionButtons : VisualElement
|
||||
{
|
||||
static readonly string k_LinkButtonLabel = L10n.Tr("Link");
|
||||
static readonly string k_UnlinkButtonLabel = L10n.Tr("Unlink");
|
||||
static readonly string k_SplitButtonLabel = L10n.Tr("Split");
|
||||
static readonly string k_JoinButtonLabel = L10n.Tr("Join");
|
||||
static readonly string k_ReverseFlowButtonLabel = L10n.Tr("Reverse Spline Flow");
|
||||
|
||||
static readonly string k_LinkButtonTooltip = L10n.Tr("Set knots to the same position and link them.");
|
||||
static readonly string k_UnlinkButtonTooltip = L10n.Tr("Unlink knots.");
|
||||
static readonly string k_SplitButtonTooltip = L10n.Tr("Divide a knot with two segments into two knots.");
|
||||
static readonly string k_JoinButtonTooltip = L10n.Tr("Connect the ends of two splines to each other.");
|
||||
static readonly string k_ReverseFlowButtonTooltip = L10n.Tr("Reverse the direction of a spline.");
|
||||
|
||||
static readonly List<SelectableKnot> m_KnotBuffer = new List<SelectableKnot>();
|
||||
|
||||
IReadOnlyList<SplineInfo> m_SelectedSplines = new List<SplineInfo>();
|
||||
|
||||
readonly Button m_LinkButton;
|
||||
readonly Button m_UnlinkButton;
|
||||
readonly Button m_SplitButton;
|
||||
readonly Button m_JoinButton;
|
||||
readonly Button m_ReverseFlowButton;
|
||||
|
||||
public SplineActionButtons()
|
||||
{
|
||||
#if !UNITY_2023_2_OR_NEWER
|
||||
Add(new Separator());
|
||||
|
||||
style.flexDirection = FlexDirection.Column;
|
||||
|
||||
var firstRow = new VisualElement();
|
||||
Add(firstRow);
|
||||
firstRow.AddToClassList("button-strip");
|
||||
firstRow.style.flexDirection = FlexDirection.Row;
|
||||
|
||||
m_LinkButton = new Button();
|
||||
m_LinkButton.text = k_LinkButtonLabel;
|
||||
m_LinkButton.tooltip = k_LinkButtonTooltip;
|
||||
m_LinkButton.style.flexGrow = new StyleFloat(1);
|
||||
m_LinkButton.clicked += OnLinkClicked;
|
||||
m_LinkButton.AddToClassList("button-strip-button");
|
||||
m_LinkButton.AddToClassList("button-strip-button--left");
|
||||
firstRow.Add(m_LinkButton);
|
||||
|
||||
m_UnlinkButton = new Button();
|
||||
m_UnlinkButton.text = k_UnlinkButtonLabel;
|
||||
m_UnlinkButton.tooltip = k_UnlinkButtonTooltip;
|
||||
m_UnlinkButton.style.flexGrow = new StyleFloat(1);
|
||||
m_UnlinkButton.clicked += OnUnlinkClicked;
|
||||
m_UnlinkButton.AddToClassList("button-strip-button");
|
||||
m_UnlinkButton.AddToClassList("button-strip-button--right");
|
||||
m_UnlinkButton.AddToClassList("button-strip-button--right-spacer");
|
||||
firstRow.Add(m_UnlinkButton);
|
||||
|
||||
m_SplitButton = new Button();
|
||||
m_SplitButton.text = k_SplitButtonLabel;
|
||||
m_SplitButton.tooltip = k_SplitButtonTooltip;
|
||||
m_SplitButton.style.flexGrow = 1;
|
||||
m_SplitButton.clicked += OnSplitClicked;
|
||||
m_SplitButton.AddToClassList("button-strip-button");
|
||||
m_SplitButton.AddToClassList("button-strip-button--left");
|
||||
m_SplitButton.AddToClassList("button-strip-button--left-spacer");
|
||||
firstRow.Add(m_SplitButton);
|
||||
|
||||
m_JoinButton = new Button();
|
||||
m_JoinButton.text = k_JoinButtonLabel;
|
||||
m_JoinButton.tooltip = k_JoinButtonTooltip;
|
||||
m_JoinButton.style.flexGrow = 1;
|
||||
m_JoinButton.clicked += OnJoinClicked;
|
||||
m_JoinButton.AddToClassList("button-strip-button");
|
||||
m_JoinButton.AddToClassList("button-strip-button--right");
|
||||
firstRow.Add(m_JoinButton);
|
||||
|
||||
m_ReverseFlowButton = new Button();
|
||||
m_ReverseFlowButton.text = k_ReverseFlowButtonLabel;
|
||||
m_ReverseFlowButton.tooltip = k_ReverseFlowButtonTooltip;
|
||||
m_ReverseFlowButton.style.flexGrow = 1;
|
||||
m_ReverseFlowButton.clicked += OnReverseFlowClicked;
|
||||
Add(m_ReverseFlowButton);
|
||||
#endif
|
||||
}
|
||||
|
||||
void OnLinkClicked()
|
||||
{
|
||||
SplineSelection.GetElements(m_SelectedSplines, m_KnotBuffer);
|
||||
EditorSplineUtility.LinkKnots(m_KnotBuffer);
|
||||
RefreshSelection(m_SelectedSplines);
|
||||
}
|
||||
|
||||
void OnUnlinkClicked()
|
||||
{
|
||||
SplineSelection.GetElements(m_SelectedSplines, m_KnotBuffer);
|
||||
EditorSplineUtility.UnlinkKnots(m_KnotBuffer);
|
||||
RefreshSelection(m_SelectedSplines);
|
||||
}
|
||||
|
||||
void OnSplitClicked()
|
||||
{
|
||||
EditorSplineUtility.RecordSelection("Split knot");
|
||||
SplineSelection.Set(EditorSplineUtility.SplitKnot(m_KnotBuffer[0]));
|
||||
}
|
||||
|
||||
void OnJoinClicked()
|
||||
{
|
||||
EditorSplineUtility.RecordSelection("Join knot");
|
||||
SplineSelection.Set(EditorSplineUtility.JoinKnots(m_KnotBuffer[0], m_KnotBuffer[1]));
|
||||
}
|
||||
|
||||
public void RefreshSelection(IReadOnlyList<SplineInfo> selectedSplines)
|
||||
{
|
||||
#if !UNITY_2023_2_OR_NEWER
|
||||
SplineSelection.GetElements(selectedSplines, m_KnotBuffer);
|
||||
|
||||
m_LinkButton.SetEnabled(SplineSelectionUtility.CanLinkKnots(m_KnotBuffer));
|
||||
m_UnlinkButton.SetEnabled(SplineSelectionUtility.CanUnlinkKnots(m_KnotBuffer));
|
||||
|
||||
m_SplitButton.SetEnabled(SplineSelectionUtility.CanSplitSelection(m_KnotBuffer));
|
||||
m_JoinButton.SetEnabled(SplineSelectionUtility.CanJoinSelection(m_KnotBuffer));
|
||||
|
||||
m_SelectedSplines = selectedSplines;
|
||||
#endif
|
||||
}
|
||||
|
||||
void OnReverseFlowClicked()
|
||||
{
|
||||
EditorSplineUtility.RecordSelection("Reverse Selected Splines Flow");
|
||||
EditorSplineUtility.ReverseSplinesFlow(m_SelectedSplines);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 077bfa6de2cd5634cb02f03ec290191d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,19 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Splines;
|
||||
|
||||
namespace UnityEditor.Splines
|
||||
{
|
||||
[CustomPropertyDrawer(typeof(SplineDataDictionary<>))]
|
||||
class SplineDataDictionaryPropertyDrawer : PropertyDrawer
|
||||
{
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
return EditorGUI.GetPropertyHeight(property.FindPropertyRelative("m_Data")) + EditorGUIUtility.standardVerticalSpacing;
|
||||
}
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
EditorGUI.PropertyField(position, property.FindPropertyRelative("m_Data"), label);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e526ebdb8c34af5b98d5eb45d4e56a5
|
||||
timeCreated: 1673297892
|
||||
@@ -0,0 +1,110 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Splines;
|
||||
|
||||
namespace UnityEditor.Splines
|
||||
{
|
||||
[CustomPropertyDrawer(typeof(SplineData<>))]
|
||||
class SplineDataPropertyDrawer : PropertyDrawer
|
||||
{
|
||||
static readonly string k_MultiSplineEditMessage = L10n.Tr("Multi-selection is not supported for SplineData");
|
||||
static readonly string k_DataUnitTooltip = L10n.Tr("The unit Data Points are using to be associated to the spline. 'Spline Distance' is " +
|
||||
"using the distance in Unity Units from the spline origin, 'Normalized Distance' is using a normalized value of the spline " +
|
||||
"length between [0,1] and Knot Index is using Spline Knot indices ");
|
||||
|
||||
readonly static GUIContent[] k_PathUnitIndexLabels = new[]
|
||||
{
|
||||
new GUIContent(L10n.Tr("Spline Distance")),
|
||||
new GUIContent(L10n.Tr("Normalized Distance")),
|
||||
new GUIContent(L10n.Tr("Knot Index"))
|
||||
};
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
float height = EditorGUIUtility.singleLineHeight;
|
||||
if (!property.isExpanded || property.serializedObject.isEditingMultipleObjects)
|
||||
return height;
|
||||
|
||||
//Adding space for the object field
|
||||
height += EditorGUIUtility.standardVerticalSpacing ;
|
||||
height += EditorGUI.GetPropertyHeight(property.FindPropertyRelative("m_DefaultValue")) + EditorGUIUtility.standardVerticalSpacing ;
|
||||
height += EditorGUI.GetPropertyHeight(property.FindPropertyRelative("m_IndexUnit")) + EditorGUIUtility.standardVerticalSpacing;
|
||||
|
||||
var datapointsProperty = property.FindPropertyRelative("m_DataPoints");
|
||||
height += EditorGUIUtility.singleLineHeight;
|
||||
if (datapointsProperty.isExpanded)
|
||||
{
|
||||
height += 2 * EditorGUIUtility.singleLineHeight;
|
||||
var arraySize = datapointsProperty.arraySize;
|
||||
if (arraySize == 0)
|
||||
{
|
||||
height += EditorGUIUtility.singleLineHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int dataPointIndex = 0; dataPointIndex < arraySize; dataPointIndex++)
|
||||
{
|
||||
height += datapointsProperty.GetArrayElementAtIndex(dataPointIndex).isExpanded
|
||||
? 3 * EditorGUIUtility.singleLineHeight + 2 * EditorGUIUtility.standardVerticalSpacing
|
||||
: EditorGUIUtility.singleLineHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
EditorGUI.BeginProperty(position, label, property);
|
||||
if (property.serializedObject.isEditingMultipleObjects)
|
||||
{
|
||||
EditorGUI.LabelField(position, L10n.Tr(k_MultiSplineEditMessage), EditorStyles.helpBox);
|
||||
return;
|
||||
}
|
||||
|
||||
property.isExpanded = EditorGUI.Foldout(SplineGUIUtility.ReserveSpace(EditorGUIUtility.singleLineHeight, ref position), property.isExpanded, label);
|
||||
++EditorGUI.indentLevel;
|
||||
if (property.isExpanded)
|
||||
{
|
||||
SplineGUIUtility.ReserveSpace(EditorGUIUtility.standardVerticalSpacing, ref position);
|
||||
|
||||
var valueProperty = property.FindPropertyRelative("m_DefaultValue");
|
||||
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpace(EditorGUI.GetPropertyHeight(valueProperty), ref position), valueProperty);
|
||||
SplineGUIUtility.ReserveSpace(EditorGUIUtility.standardVerticalSpacing, ref position);
|
||||
|
||||
var indexProperty = property.FindPropertyRelative("m_IndexUnit");
|
||||
var dataPointsProperty = property.FindPropertyRelative("m_DataPoints");
|
||||
var pathUnit = (PathIndexUnit)indexProperty.intValue;
|
||||
EditorGUI.BeginChangeCheck();
|
||||
var newPathUnit = EditorGUI.Popup(SplineGUIUtility.ReserveSpace(EditorGUI.GetPropertyHeight(indexProperty), ref position),
|
||||
new GUIContent("Data Index Unit", L10n.Tr(k_DataUnitTooltip)), (int)pathUnit, k_PathUnitIndexLabels);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
if (dataPointsProperty.arraySize == 0)
|
||||
indexProperty.intValue = newPathUnit;
|
||||
else
|
||||
{
|
||||
SplineDataConversionWindow.DoConfirmWindow(
|
||||
position,
|
||||
property,
|
||||
property.serializedObject.targetObject as Component,
|
||||
newPathUnit,
|
||||
false);
|
||||
GUIUtility.ExitGUI();
|
||||
}
|
||||
}
|
||||
|
||||
SplineGUIUtility.ReserveSpace(EditorGUIUtility.standardVerticalSpacing, ref position);
|
||||
|
||||
dataPointsProperty.isExpanded = EditorGUI.Foldout(SplineGUIUtility.ReserveSpace(EditorGUIUtility.singleLineHeight, ref position), dataPointsProperty.isExpanded, new GUIContent("Data Points"));
|
||||
if (dataPointsProperty.isExpanded)
|
||||
{
|
||||
SplineDataReorderableListUtility
|
||||
.GetDataPointsReorderableList(property, dataPointsProperty, pathUnit).DoList(position);
|
||||
}
|
||||
}
|
||||
--EditorGUI.indentLevel;
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db00a1694ad7319449442045b8557a93
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Splines;
|
||||
|
||||
namespace UnityEditor.Splines
|
||||
{
|
||||
[InitializeOnLoad]
|
||||
static class SplineDataReorderableListUtility
|
||||
{
|
||||
readonly static string k_DataIndexTooltip = L10n.Tr("The index of the Data Point along the spline and the unit used.");
|
||||
readonly static string k_DataValueTooltip = L10n.Tr("The value of the Data Point.");
|
||||
|
||||
static Dictionary<string, ReorderableList> s_ReorderableLists = new Dictionary<string, ReorderableList>();
|
||||
static PathIndexUnit s_PathIndexUnit;
|
||||
readonly static string[] k_DisplayName = new string[] {"Dist","Path %","Knot"};
|
||||
|
||||
static SplineDataReorderableListUtility()
|
||||
{
|
||||
Selection.selectionChanged += ClearReorderableLists;
|
||||
}
|
||||
|
||||
static void ClearReorderableLists()
|
||||
{
|
||||
s_ReorderableLists.Clear();
|
||||
}
|
||||
|
||||
static void SetSplineDataDirty(SerializedProperty splineDataProperty)
|
||||
{
|
||||
var splineDataObject = SerializedPropertyUtility.GetSerializedPropertyObject(splineDataProperty);
|
||||
var dirtyMethod = splineDataObject.GetType().GetMethod("SetDirty", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
dirtyMethod?.Invoke(splineDataObject, null);
|
||||
}
|
||||
|
||||
public static ReorderableList GetDataPointsReorderableList(SerializedProperty splineDataProperty, SerializedProperty dataPointProperty, PathIndexUnit unit)
|
||||
{
|
||||
var key = dataPointProperty.propertyPath + splineDataProperty.serializedObject.targetObject.GetInstanceID();
|
||||
s_PathIndexUnit = unit;
|
||||
|
||||
if(s_ReorderableLists.TryGetValue(key, out var list))
|
||||
{
|
||||
try
|
||||
{
|
||||
SerializedProperty.EqualContents(list.serializedProperty, dataPointProperty);
|
||||
return list;
|
||||
}
|
||||
catch(NullReferenceException)
|
||||
{
|
||||
s_ReorderableLists.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
list = new ReorderableList(dataPointProperty.serializedObject, dataPointProperty, false, false, true, true);
|
||||
s_ReorderableLists.Add(key, list);
|
||||
|
||||
list.elementHeightCallback = (int index) =>
|
||||
{
|
||||
return dataPointProperty.arraySize > 0 && dataPointProperty.GetArrayElementAtIndex(index).isExpanded
|
||||
? 3 * EditorGUIUtility.singleLineHeight + 2 * EditorGUIUtility.standardVerticalSpacing
|
||||
: EditorGUIUtility.singleLineHeight;
|
||||
};
|
||||
|
||||
list.onChangedCallback = reorderableList =>
|
||||
{
|
||||
SetSplineDataDirty(splineDataProperty);
|
||||
};
|
||||
|
||||
list.drawElementCallback =
|
||||
(Rect position, int listIndex, bool isActive, bool isFocused) =>
|
||||
{
|
||||
var ppte = dataPointProperty.GetArrayElementAtIndex(listIndex);
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
var expended = EditorGUI.Foldout(SplineGUIUtility.ReserveSpace(EditorGUIUtility.singleLineHeight, ref position), ppte.isExpanded, new GUIContent($"Data Point [{listIndex}]"), true);
|
||||
if (expended != ppte.isExpanded)
|
||||
{
|
||||
ppte.isExpanded = expended;
|
||||
if (!isActive)
|
||||
list.index = listIndex;
|
||||
list.GrabKeyboardFocus();
|
||||
}
|
||||
|
||||
if (ppte.isExpanded)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
SplineGUIUtility.ReserveSpace(EditorGUIUtility.standardVerticalSpacing, ref position);
|
||||
EditorGUI.BeginChangeCheck();
|
||||
var indexProperty = ppte.FindPropertyRelative("m_Index");
|
||||
EditorGUI.DelayedFloatField(SplineGUIUtility.ReserveSpace(EditorGUIUtility.singleLineHeight, ref position), indexProperty, new GUIContent($"Data Index ({k_DisplayName[(int)s_PathIndexUnit]})", L10n.Tr(k_DataIndexTooltip)));
|
||||
if(EditorGUI.EndChangeCheck())
|
||||
{
|
||||
if (!isActive)
|
||||
return;
|
||||
|
||||
Undo.SetCurrentGroupName("Modify Spline Data point index");
|
||||
// Apply data point's index change - needs to be applied here so that the sort below is correct.
|
||||
dataPointProperty.serializedObject.ApplyModifiedProperties();
|
||||
var newIndex = ppte.FindPropertyRelative("m_Index").floatValue;
|
||||
|
||||
var splineDataObject = SerializedPropertyUtility.GetSerializedPropertyObject(splineDataProperty);
|
||||
var sortMethod = splineDataObject.GetType().GetMethod("ForceSort", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
|
||||
sortMethod?.Invoke(splineDataObject, null);
|
||||
dataPointProperty.serializedObject.Update();
|
||||
for (int i = 0; i < dataPointProperty.arraySize; i++)
|
||||
{
|
||||
var index = dataPointProperty.GetArrayElementAtIndex(i).FindPropertyRelative("m_Index").floatValue;
|
||||
if (index == newIndex)
|
||||
{
|
||||
list.index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the changes resulting from data point sort.
|
||||
dataPointProperty.serializedObject.ApplyModifiedProperties();
|
||||
|
||||
SetSplineDataDirty(splineDataProperty);
|
||||
}
|
||||
|
||||
SplineGUIUtility.ReserveSpace(EditorGUIUtility.standardVerticalSpacing, ref position);
|
||||
EditorGUI.BeginChangeCheck();
|
||||
var valueProperty = ppte.FindPropertyRelative("m_Value");
|
||||
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpace(EditorGUI.GetPropertyHeight(valueProperty), ref position), valueProperty, new GUIContent("Data Value", L10n.Tr(k_DataValueTooltip)));
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
Undo.SetCurrentGroupName("Modify Spline Data point value");
|
||||
SetSplineDataDirty(splineDataProperty);
|
||||
if (!isActive)
|
||||
list.index = listIndex;
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5bcefd1f1914dff4dae9fe9c4250b8da
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,47 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Splines;
|
||||
|
||||
namespace UnityEditor.Splines
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a property drawer for <see cref="SplineInfo"/> types.
|
||||
/// </summary>
|
||||
[CustomPropertyDrawer(typeof(SplineInfo))]
|
||||
public class SplineInfoPropertyDrawer : PropertyDrawer
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the height of a SerializedProperty in pixels.
|
||||
/// </summary>
|
||||
/// <param name="property">The SerializedProperty to calculate height for.</param>
|
||||
/// <param name="label">The label of the SerializedProperty.</param>
|
||||
/// <returns>Returns the height of a SerializedProperty in pixels.</returns>
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
return EditorStyles.popup.CalcSize(label).y * 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an interface for a SerializedProperty with an integer property type.
|
||||
/// </summary>
|
||||
/// <param name="position">Rectangle on the screen to use for the property GUI.</param>
|
||||
/// <param name="property">The SerializedProperty to make the custom GUI for.</param>
|
||||
/// <param name="label">The label of this property.</param>
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
var obj = property.FindPropertyRelative("m_Object");
|
||||
var con = property.FindPropertyRelative("m_Container");
|
||||
var ind = property.FindPropertyRelative("m_SplineIndex");
|
||||
|
||||
if (con.managedReferenceValue != null && obj.objectReferenceValue == null)
|
||||
EditorGUI.LabelField(SplineGUIUtility.ReserveSpaceForLine(ref position), "ISplineContainer",
|
||||
property.managedReferenceFieldTypename);
|
||||
else
|
||||
{
|
||||
EditorGUI.ObjectField(SplineGUIUtility.ReserveSpaceForLine(ref position), obj, typeof(ISplineContainer),
|
||||
new GUIContent("Spline Container"));
|
||||
}
|
||||
|
||||
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpaceForLine(ref position), ind);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 00a9f482c3791427aad5ae45ad463eda
|
||||
timeCreated: 1671639448
|
||||
@@ -0,0 +1,70 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Splines;
|
||||
|
||||
namespace UnityEditor.Splines
|
||||
{
|
||||
[CustomPropertyDrawer(typeof(Spline))]
|
||||
class SplinePropertyDrawer : PropertyDrawer
|
||||
{
|
||||
static readonly string[] k_SplineData = new string[]
|
||||
{
|
||||
"m_IntData",
|
||||
"m_FloatData",
|
||||
"m_Float4Data",
|
||||
"m_ObjectData"
|
||||
};
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
if (!property.isExpanded)
|
||||
return SplineGUIUtility.lineHeight;
|
||||
float height = SplineGUIUtility.lineHeight * 2;
|
||||
height += KnotReorderableList.Get(property).GetHeight();
|
||||
height += EditorGUIUtility.standardVerticalSpacing;
|
||||
for (int i = 0, c = k_SplineData.Length; i < c; ++i)
|
||||
height += EditorGUI.GetPropertyHeight(property.FindPropertyRelative(k_SplineData[i]));
|
||||
return height;
|
||||
}
|
||||
|
||||
// Important note - if this is inspecting a Spline that is not part of an ISplineContainer, callbacks will not
|
||||
// invoked. That means the Scene View won't reflect changes made in the Inspector without additional code to
|
||||
// fire the modified callbacks. The easiest way to handle this is to implement ISplineContainer. Alternatively,
|
||||
// write a custom editor for your class and call Spline.EnforceTangentModeNoNotify() & Spline.SetDirty() after
|
||||
// any changes.
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
property.isExpanded = EditorGUI.Foldout(
|
||||
SplineGUIUtility.ReserveSpace(SplineGUIUtility.lineHeight, ref position),
|
||||
property.isExpanded, label);
|
||||
|
||||
if(property.isExpanded)
|
||||
{
|
||||
var closedProperty = property.FindPropertyRelative("m_Closed");
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
EditorGUI.PropertyField(SplineGUIUtility.ReserveSpace(SplineGUIUtility.lineHeight, ref position), closedProperty);
|
||||
if (EditorGUI.EndChangeCheck() && SerializedPropertyUtility.TryGetSpline(property, out var spline))
|
||||
{
|
||||
property.serializedObject.ApplyModifiedProperties();
|
||||
spline.CheckAutoSmoothExtremityKnots();
|
||||
spline.SetDirty(SplineModification.ClosedModified);
|
||||
property.serializedObject.Update();
|
||||
}
|
||||
|
||||
var knots = KnotReorderableList.Get(property);
|
||||
knots.DoList(position);
|
||||
position.y += knots.GetHeight() + EditorGUIUtility.standardVerticalSpacing;
|
||||
|
||||
for (int i = 0, c = k_SplineData.Length; i < c; ++i)
|
||||
{
|
||||
var prop = property.FindPropertyRelative(k_SplineData[i]);
|
||||
var height = EditorGUI.GetPropertyHeight(prop);
|
||||
var rect = position;
|
||||
rect.height = height;
|
||||
position.y += rect.height;
|
||||
EditorGUI.PropertyField(rect, prop, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5dc161b954c4dd6a149b642c8e53bf9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,155 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Splines;
|
||||
|
||||
namespace UnityEditor.Splines
|
||||
{
|
||||
class SplineReorderableList : ReorderableList
|
||||
{
|
||||
SplineContainer m_Container;
|
||||
|
||||
static Dictionary<int, SplineReorderableList> s_Pool = new();
|
||||
|
||||
static SplineReorderableList()
|
||||
{
|
||||
Selection.selectionChanged += ClearPool;
|
||||
}
|
||||
|
||||
static int GetPropertyHash(SerializedProperty prop)
|
||||
{
|
||||
if(prop.serializedObject.targetObject == null)
|
||||
return 0;
|
||||
|
||||
unchecked
|
||||
{
|
||||
int hash = prop.serializedObject.targetObject.GetInstanceID();
|
||||
hash = hash * 31 * prop.propertyPath.GetHashCode();
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
static void ClearPool() => s_Pool.Clear();
|
||||
|
||||
public static SplineReorderableList Get(SerializedProperty splineArrayElement)
|
||||
{
|
||||
int hash = GetPropertyHash(splineArrayElement);
|
||||
if (!s_Pool.TryGetValue(hash, out var list))
|
||||
s_Pool.Add(hash, list = new SplineReorderableList(splineArrayElement.serializedObject, splineArrayElement));
|
||||
list.Init(splineArrayElement);
|
||||
return list;
|
||||
}
|
||||
|
||||
void Init(SerializedProperty splineArrayElement)
|
||||
{
|
||||
serializedProperty = splineArrayElement;
|
||||
if (splineArrayElement.serializedObject.targetObject is SplineContainer container)
|
||||
m_Container = container;
|
||||
}
|
||||
|
||||
public SplineReorderableList(
|
||||
SerializedObject serializedObject,
|
||||
SerializedProperty splineArrayElement) : base(serializedObject, splineArrayElement, true, false, true, true)
|
||||
{
|
||||
Init(splineArrayElement);
|
||||
multiSelect = true;
|
||||
elementHeightCallback += GetElementHeight;
|
||||
drawElementCallback += DrawElement;
|
||||
onReorderCallbackWithDetails += OnReorder;
|
||||
onSelectCallback += OnSelect;
|
||||
onAddCallback += OnAdd;
|
||||
onRemoveCallback += OnRemove;
|
||||
}
|
||||
|
||||
float GetElementHeight(int i)
|
||||
{
|
||||
return EditorGUI.GetPropertyHeight(serializedProperty.GetArrayElementAtIndex(i));
|
||||
}
|
||||
|
||||
void DrawElement(Rect position, int listIndex, bool isactive, bool isfocused)
|
||||
{
|
||||
++EditorGUI.indentLevel;
|
||||
EditorGUI.BeginChangeCheck();
|
||||
EditorGUI.PropertyField(position,
|
||||
serializedProperty.GetArrayElementAtIndex(listIndex),
|
||||
new GUIContent($"Spline {listIndex}"));
|
||||
EditorGUI.EndChangeCheck();
|
||||
--EditorGUI.indentLevel;
|
||||
}
|
||||
|
||||
void OnReorder(ReorderableList reorderableList, int srcIndex, int dstIndex)
|
||||
{
|
||||
if (m_Container == null)
|
||||
return;
|
||||
|
||||
Undo.RegisterCompleteObjectUndo(serializedProperty.serializedObject.targetObject, "Reordering Spline in SplineContainer");
|
||||
m_Container.ReorderSpline(srcIndex, dstIndex);
|
||||
serializedProperty.serializedObject.ApplyModifiedProperties();
|
||||
serializedProperty.serializedObject.Update();
|
||||
}
|
||||
|
||||
void OnSelect(ReorderableList _)
|
||||
{
|
||||
if (m_Container == null)
|
||||
return;
|
||||
SplineSelection.SetInspectorSelectedSplines(m_Container, selectedIndices);
|
||||
SceneView.RepaintAll();
|
||||
}
|
||||
|
||||
// Used in tests
|
||||
internal void OnAdd(ReorderableList _)
|
||||
{
|
||||
if(m_Container == null)
|
||||
return;
|
||||
|
||||
Undo.RegisterCompleteObjectUndo(serializedProperty.serializedObject.targetObject, "Add new Spline");
|
||||
|
||||
int added = 0;
|
||||
|
||||
if(selectedIndices.Count > 0)
|
||||
{
|
||||
foreach(var i in selectedIndices)
|
||||
{
|
||||
var spline = m_Container.AddSpline();
|
||||
spline.Copy(m_Container.Splines[i]);
|
||||
m_Container.CopyKnotLinks(i, m_Container.Splines.Count - 1);
|
||||
added++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var spline = m_Container.AddSpline();
|
||||
var srcSplineIndex = m_Container.Splines.Count - 2;
|
||||
if (srcSplineIndex >= 0)
|
||||
{
|
||||
spline.Copy(m_Container.Splines[srcSplineIndex]);
|
||||
m_Container.CopyKnotLinks(srcSplineIndex, m_Container.Splines.Count - 1);
|
||||
}
|
||||
added++;
|
||||
}
|
||||
|
||||
int maxCount = m_Container.Splines.Count;
|
||||
SelectRange(maxCount - added, maxCount - 1);
|
||||
onSelectCallback(this);
|
||||
}
|
||||
|
||||
// Used in tests
|
||||
internal void OnRemove(ReorderableList _)
|
||||
{
|
||||
if (m_Container == null)
|
||||
return;
|
||||
|
||||
Undo.RegisterCompleteObjectUndo(serializedProperty.serializedObject.targetObject, "Removing Spline from SplineContainer");
|
||||
|
||||
for (int i = selectedIndices.Count - 1; i >= 0; i--)
|
||||
m_Container.RemoveSplineAt(selectedIndices[i]);
|
||||
|
||||
PrefabUtility.RecordPrefabInstancePropertyModifications(serializedProperty.serializedObject.targetObject);
|
||||
|
||||
ClearSelection();
|
||||
SplineSelection.ClearInspectorSelectedSplines();
|
||||
|
||||
SceneView.RepaintAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0dda34381c8748a7a8d49edc59ba0847
|
||||
timeCreated: 1669140420
|
||||
@@ -0,0 +1,140 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.Splines;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEditor.Splines
|
||||
{
|
||||
sealed class TangentModePropertyField<T> : VisualElement
|
||||
where T : ISelectableElement
|
||||
{
|
||||
const string k_ButtonStripUssClass = "button-strip";
|
||||
const string k_ButtonStripButtonUssClass = k_ButtonStripUssClass + "-button";
|
||||
const string k_ButtonStripButtonLeftUssClass = k_ButtonStripButtonUssClass + "--left";
|
||||
const string k_ButtonStripButtonMiddleUssClass = k_ButtonStripButtonUssClass + "--middle";
|
||||
const string k_ButtonStripButtonRightUssClass = k_ButtonStripButtonUssClass + "--right";
|
||||
const string k_ButtonStripButtonIconUssClass = k_ButtonStripButtonUssClass + "__icon";
|
||||
const string k_ButtonStripButtonTextUssClass = k_ButtonStripButtonUssClass + "__text";
|
||||
const string k_ButtonStripButtonCheckedUssClass = k_ButtonStripButtonUssClass + "--checked";
|
||||
|
||||
static readonly SplineGUIUtility.EqualityComparer<T> s_Comparer = (a, b) =>
|
||||
GetModeForProperty(EditorSplineUtility.GetKnot(a).Mode) ==
|
||||
GetModeForProperty(EditorSplineUtility.GetKnot(b).Mode);
|
||||
|
||||
public event Action changed;
|
||||
|
||||
const TangentMode k_BezierMode = TangentMode.Mirrored;
|
||||
|
||||
readonly Button m_LinearButton;
|
||||
readonly Button m_AutoSmoothButton;
|
||||
readonly Button m_BezierButton;
|
||||
IReadOnlyList<T> m_Elements = new List<T>(0);
|
||||
TangentMode m_CurrentMode;
|
||||
|
||||
internal TangentModePropertyField()
|
||||
{
|
||||
AddToClassList(k_ButtonStripUssClass);
|
||||
|
||||
Add(m_LinearButton = CreateButton("Linear", L10n.Tr("Linear"), L10n.Tr("Tangents are pointing to the previous/next spline knot.")));
|
||||
m_LinearButton.AddToClassList(k_ButtonStripButtonLeftUssClass);
|
||||
m_LinearButton.clickable.clicked += () => OnValueChange(TangentMode.Linear);
|
||||
|
||||
Add(m_AutoSmoothButton = CreateButton("AutoSmooth", L10n.Tr("Auto"), L10n.Tr("Tangents are calculated using the previous and next knot positions.")));
|
||||
m_AutoSmoothButton.AddToClassList(k_ButtonStripButtonMiddleUssClass);
|
||||
m_AutoSmoothButton.clickable.clicked += () => OnValueChange(TangentMode.AutoSmooth);
|
||||
|
||||
Add(m_BezierButton = CreateButton("Bezier", L10n.Tr("Bezier"), L10n.Tr("Tangents are customizable and modifiable.")));
|
||||
m_BezierButton.AddToClassList(k_ButtonStripButtonRightUssClass);
|
||||
m_BezierButton.clickable.clicked += () => OnValueChange(TangentMode.Mirrored);
|
||||
}
|
||||
|
||||
static Button CreateButton(string name, string text, string tooltip)
|
||||
{
|
||||
var button = new Button{name = name};
|
||||
button.tooltip = tooltip;
|
||||
button.AddToClassList(k_ButtonStripButtonUssClass);
|
||||
|
||||
var icon = new VisualElement();
|
||||
icon.AddToClassList(k_ButtonStripButtonIconUssClass);
|
||||
icon.pickingMode = PickingMode.Ignore;
|
||||
button.Add(icon);
|
||||
|
||||
var label = new TextElement();
|
||||
label.AddToClassList(k_ButtonStripButtonTextUssClass);
|
||||
label.pickingMode = PickingMode.Ignore;
|
||||
label.text = text;
|
||||
button.Add(label);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
public void Update(IReadOnlyList<T> targets)
|
||||
{
|
||||
m_Elements = targets;
|
||||
|
||||
if (SplineGUIUtility.HasMultipleValues(m_Elements, s_Comparer))
|
||||
SetToMixedValuesState();
|
||||
else
|
||||
SetValueWithoutNotify(EditorSplineUtility.GetKnot(targets[0]).Mode);
|
||||
}
|
||||
|
||||
void SetToMixedValuesState()
|
||||
{
|
||||
SetValueWithoutNotify((TangentMode)(-1));
|
||||
}
|
||||
|
||||
void SetValueWithoutNotify(TangentMode mode)
|
||||
{
|
||||
m_CurrentMode = GetModeForProperty(mode);
|
||||
|
||||
SetButtonChecked(m_LinearButton, m_CurrentMode == TangentMode.Linear);
|
||||
SetButtonChecked(m_AutoSmoothButton, m_CurrentMode == TangentMode.AutoSmooth);
|
||||
SetButtonChecked(m_BezierButton, m_CurrentMode == k_BezierMode);
|
||||
}
|
||||
void SetButtonChecked(Button button, bool isChecked)
|
||||
{
|
||||
button.EnableInClassList(k_ButtonStripButtonCheckedUssClass, isChecked);
|
||||
}
|
||||
|
||||
void OnValueChange(TangentMode mode)
|
||||
{
|
||||
if (GetModeForProperty(mode) == m_CurrentMode)
|
||||
return;
|
||||
|
||||
SetValueWithoutNotify(mode);
|
||||
|
||||
EditorSplineUtility.RecordSelection(SplineInspectorOverlay.SplineChangeUndoMessage);
|
||||
for (int i = 0; i < m_Elements.Count; ++i)
|
||||
{
|
||||
var knot = EditorSplineUtility.GetKnot(m_Elements[i]);
|
||||
if (m_Elements[i] is SelectableTangent tangent)
|
||||
knot.SetTangentMode(mode, (BezierTangent)tangent.TangentIndex);
|
||||
else
|
||||
{
|
||||
// Continuous mode should be prefered instead of Mirrored when switching from AutoSmooth to Bezier
|
||||
// as Centripetal Catmull-Rom's tangents are not always of equal length.
|
||||
if (knot.Mode == TangentMode.AutoSmooth && mode == TangentMode.Mirrored)
|
||||
knot.Mode = TangentMode.Continuous;
|
||||
else
|
||||
knot.Mode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
changed?.Invoke();
|
||||
}
|
||||
|
||||
static TangentMode GetModeForProperty(TangentMode mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case TangentMode.Continuous:
|
||||
case TangentMode.Mirrored:
|
||||
case TangentMode.Broken:
|
||||
return k_BezierMode;
|
||||
|
||||
default:
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c74ddef7e6e74f529a51b5ad71552fbd
|
||||
timeCreated: 1637605071
|
||||
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine.Splines;
|
||||
using UnityEngine.UIElements;
|
||||
#if !UNITY_2022_1_OR_NEWER
|
||||
using UnityEditor.UIElements;
|
||||
#endif
|
||||
|
||||
namespace UnityEditor.Splines
|
||||
{
|
||||
class TangentPropertyField : VisualElement
|
||||
{
|
||||
static readonly List<float> s_LengthBuffer = new List<float>(0);
|
||||
static readonly SplineGUIUtility.EqualityComparer<float> s_MagnitudeComparer = (a, b) => a.Equals(b);
|
||||
|
||||
const string k_TangentFoldoutStyle = "tangent-drawer";
|
||||
const string k_TangentMagnitudeFloatFieldStyle = "tangent-magnitude-floatfield";
|
||||
|
||||
readonly BezierTangent m_Direction;
|
||||
readonly FloatField m_Magnitude;
|
||||
public readonly Float3PropertyField<SelectableKnot> vector3field;
|
||||
|
||||
IReadOnlyList<SelectableKnot> m_Elements = new List<SelectableKnot>(0);
|
||||
|
||||
public event Action changed;
|
||||
|
||||
public TangentPropertyField(string text, string vect3name, BezierTangent direction)
|
||||
{
|
||||
m_Direction = direction;
|
||||
|
||||
//Create Elements
|
||||
AddToClassList(k_TangentFoldoutStyle);
|
||||
AddToClassList("unity-base-field");
|
||||
|
||||
style.marginBottom = style.marginLeft = style.marginRight = style.marginTop = 0;
|
||||
|
||||
var foldout = new Foldout() { value = false };
|
||||
var foldoutToggle = foldout.Q<Toggle>();
|
||||
|
||||
m_Magnitude = new FloatField(L10n.Tr(text), 6);
|
||||
m_Magnitude.style.flexDirection = FlexDirection.Row;
|
||||
m_Magnitude.RemoveFromClassList("unity-base-field");
|
||||
vector3field = new Float3PropertyField<SelectableKnot>("", GetTangentPosition, ApplyPosition) { name = vect3name };
|
||||
|
||||
//Build UI Hierarchy
|
||||
Add(foldout);
|
||||
foldoutToggle.Add(m_Magnitude);
|
||||
foldout.Add(vector3field);
|
||||
foldout.Q<VisualElement>("unity-content").style.marginBottom = 0;
|
||||
|
||||
var field = m_Magnitude.Q<VisualElement>("unity-text-input");
|
||||
field.AddToClassList(k_TangentMagnitudeFloatFieldStyle);
|
||||
|
||||
vector3field.changed += () =>
|
||||
{
|
||||
Update(m_Elements);
|
||||
changed?.Invoke();
|
||||
};
|
||||
|
||||
m_Magnitude.RegisterValueChangedCallback((evt) =>
|
||||
{
|
||||
var value = evt.newValue;
|
||||
if (evt.newValue < 0f)
|
||||
{
|
||||
m_Magnitude.SetValueWithoutNotify(0f);
|
||||
value = 0f;
|
||||
}
|
||||
|
||||
EditorSplineUtility.RecordObjects(m_Elements, SplineInspectorOverlay.SplineChangeUndoMessage);
|
||||
for (var i = 0; i < m_Elements.Count; ++i)
|
||||
{
|
||||
var knot = m_Elements[i];
|
||||
UpdateTangentMagnitude(new SelectableTangent(knot.SplineInfo, knot.KnotIndex, m_Direction), value, m_Direction == BezierTangent.In ? -1f : 1f);
|
||||
}
|
||||
|
||||
m_Magnitude.showMixedValue = false;
|
||||
Update(m_Elements);
|
||||
changed?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
public void Update(IReadOnlyList<SelectableKnot> elements)
|
||||
{
|
||||
m_Elements = elements;
|
||||
|
||||
s_LengthBuffer.Clear();
|
||||
for (int i = 0; i < elements.Count; ++i)
|
||||
s_LengthBuffer.Add(math.length((m_Direction == BezierTangent.In ? elements[i].TangentIn : elements[i].TangentOut).LocalPosition));
|
||||
|
||||
m_Magnitude.showMixedValue = SplineGUIUtility.HasMultipleValues(s_LengthBuffer, s_MagnitudeComparer);
|
||||
if (!m_Magnitude.showMixedValue)
|
||||
m_Magnitude.SetValueWithoutNotify(s_LengthBuffer[0]);
|
||||
|
||||
vector3field.Update(elements);
|
||||
}
|
||||
|
||||
float3 GetTangentPosition(SelectableKnot knot)
|
||||
{
|
||||
return new SelectableTangent(knot.SplineInfo, knot.KnotIndex, m_Direction).LocalPosition;
|
||||
}
|
||||
|
||||
void ApplyPosition(SelectableKnot knot, float3 position)
|
||||
{
|
||||
new SelectableTangent(knot.SplineInfo, knot.KnotIndex, m_Direction) { LocalPosition = position };
|
||||
}
|
||||
|
||||
void UpdateTangentMagnitude(SelectableTangent tangent, float value, float directionSign)
|
||||
{
|
||||
var direction = new float3(0, 0, directionSign);
|
||||
if (math.length(tangent.LocalDirection) > 0)
|
||||
direction = math.normalize(tangent.LocalDirection);
|
||||
|
||||
ElementInspector.ignoreKnotCallbacks = true;
|
||||
tangent.LocalPosition = value * direction;
|
||||
ElementInspector.ignoreKnotCallbacks = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 26228ca4ee1f8eb44b0c159e0fc0ddec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97db6026b4154fbf97b957dd2137810d
|
||||
timeCreated: 1634919075
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bea6b12ab5854760b485bc57408b9c3e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54bfd57b67ce80c4481a2c3616fb0e7c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bf6af609234b6af4f8218e694d4895c4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d65cf8364d644f1a82a62a1c0272310
|
||||
timeCreated: 1671553991
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b9129ed6a1eebe48b297bed06a9694e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e21391b51918435b9e07d2f917a37c79
|
||||
timeCreated: 1675955599
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e1dec5e4d3c4dd84aab4dfbb3c8e6b48
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: facf982a205a4c93830bb62332a75ceb
|
||||
timeCreated: 1649792821
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae3c8f89802f5a242813000e2c3b55a7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user