using System; using System.Collections.Generic; using System.Linq; using UnityEditor.EditorTools; using UnityEngine; using UnityEditor.SettingsManagement; using UnityEditor.ShortcutManagement; using UnityEngine.Splines; #if UNITY_2022_1_OR_NEWER using UnityEditor.Overlays; #else using System.Reflection; using UnityEditor.Toolbars; using UnityEngine.UIElements; #endif namespace UnityEditor.Splines { /// /// Describes how the handles are oriented. Besides the default tool handle rotation settings, Global and Local, /// spline elements have the Parent and Element handle rotations. When elements are selected, a tool's handle /// rotation setting affects the behavior of some transform tools, such as the Rotate and Scale tools. /// public enum HandleOrientation { /// /// Tool handles are in the active object's rotation. /// Local = 0, /// /// Tool handles are in global rotation. /// Global = 1, /// /// Tool handles are in active element's parent's rotation. /// Parent = 2, /// /// Tool handles are in active element's rotation. /// Element = 3 } #if UNITY_2022_1_OR_NEWER public abstract class SplineToolSettings : UnityEditor.Editor, ICreateToolbar { public IEnumerable toolbarElements { #else abstract class SplineToolSettings : CreateToolbarBase { protected override IEnumerable toolbarElements { #endif get { yield return "Tool Settings/Pivot Mode"; yield return "Spline Tool Settings/Handle Rotation"; #if !UNITY_2022_1_OR_NEWER yield return "Spline Tool Settings/Handle Visuals"; #endif } } } /// /// Base class from which all Spline tools inherit. /// Inherit SplineTool to author tools that behave like native spline tools. This class implements some common /// functionality and shortcuts specific to spline authoring. /// public abstract class SplineTool : EditorTool { /// The current orientation of the handles for the tool in use. static UserSetting m_HandleOrientation = new UserSetting(PathSettings.instance, "SplineTool.HandleOrientation", HandleOrientation.Global, SettingsScope.User); /// The current orientation of the handles for the current spline tool. public static HandleOrientation handleOrientation { get => m_HandleOrientation; set { if (m_HandleOrientation != value) { m_HandleOrientation.SetValue(value, true); if(m_HandleOrientation == HandleOrientation.Local || m_HandleOrientation == HandleOrientation.Global) Tools.pivotRotation = (PivotRotation)m_HandleOrientation.value; else // If setting HandleOrientation to something else, then set the PivotRotation to global, done for GridSnapping button activation { Tools.pivotRotationChanged -= OnPivotRotationChanged; Tools.pivotRotation = PivotRotation.Local; Tools.pivotRotationChanged += OnPivotRotationChanged; } handleOrientationChanged?.Invoke(); } } } internal static event Action handleOrientationChanged; /// /// The current active SplineTool in use. /// // Workaround for lack of access to ShortcutContext. Use this to pass shortcut actions to tool instances. protected static SplineTool activeTool { get; private set; } /// /// The current position of the pivot regarding the selection. /// public static Vector3 pivotPosition => TransformOperation.pivotPosition; /// /// The current rotation of the handle regarding the selection and the Handle Rotation configuration. /// public static Quaternion handleRotation => TransformOperation.handleRotation; /// /// Updates the current handle rotation. This is usually called internally by callbacks. /// UpdateHandleRotation can be called to refresh the handle rotation after manipulating spline elements, for instance, such as rotating a knot. /// public static void UpdateHandleRotation() => TransformOperation.UpdateHandleRotation(); /// /// Updates current pivot position, usually called internally by callbacks. /// It can be called to refresh the pivot position after manipulating spline elements, for instance, such as moving a knot. /// /// /// Set to true to use the knots positions to compute the pivot instead of the tangents ones. This is necessary for /// some tools where it is preferrable to represent the handle on the knots rather than on the tangents directly. /// For instance, rotating a tangent is more intuitive when the handle is on the knot. /// public static void UpdatePivotPosition(bool useKnotPositionForTangents = false) => TransformOperation.UpdatePivotPosition(useKnotPositionForTangents); /// /// Invoked after this EditorTool becomes the active tool. /// public override void OnActivated() { SplineSelection.changed += OnSplineSelectionChanged; Spline.afterSplineWasModified += AfterSplineWasModified; Undo.undoRedoPerformed += UndoRedoPerformed; Tools.pivotRotationChanged += OnPivotRotationChanged; Tools.pivotModeChanged += OnPivotModeChanged; TransformOperation.UpdateSelection(targets); handleOrientationChanged += OnHandleOrientationChanged; activeTool = this; } /// /// Invoked before this EditorTool stops being the active tool. /// public override void OnWillBeDeactivated() { SplineSelection.changed -= OnSplineSelectionChanged; Spline.afterSplineWasModified -= AfterSplineWasModified; Undo.undoRedoPerformed -= UndoRedoPerformed; Tools.pivotRotationChanged -= OnPivotRotationChanged; Tools.pivotModeChanged -= OnPivotModeChanged; handleOrientationChanged -= OnHandleOrientationChanged; SplineToolContext.useCustomSplineHandles = false; activeTool = null; } /// /// Callback invoked when the handle rotation configuration changes. /// protected virtual void OnHandleOrientationChanged() { UpdateHandleRotation(); } static void OnPivotRotationChanged() { handleOrientation = (HandleOrientation)Tools.pivotRotation; } /// /// Callback invoked when the pivot mode configuration changes. /// protected virtual void OnPivotModeChanged() { UpdatePivotPosition(); UpdateHandleRotation(); } void AfterSplineWasModified(Spline spline) => UpdateSelection(); void UndoRedoPerformed() => UpdateSelection(); void OnSplineSelectionChanged() { UpdateSelection(); TransformOperation.pivotFreeze = TransformOperation.PivotFreeze.None; UpdateHandleRotation(); UpdatePivotPosition(); } void UpdateSelection() { TransformOperation.UpdateSelection(targets); } static void CycleTangentMode() { var elementSelection = TransformOperation.elementSelection; foreach (var element in elementSelection) { var knot = EditorSplineUtility.GetKnot(element); if (element is SelectableTangent tangent) { //Do nothing on the tangent if the knot is also in the selection if (elementSelection.Contains(tangent.Owner)) continue; bool oppositeTangentSelected = elementSelection.Contains(tangent.OppositeTangent); if (!oppositeTangentSelected) { var newMode = default(TangentMode); var previousMode = knot.Mode; if(!SplineUtility.AreTangentsModifiable(previousMode)) continue; if(previousMode == TangentMode.Mirrored) newMode = TangentMode.Continuous; if(previousMode == TangentMode.Continuous) newMode = TangentMode.Broken; if(previousMode == TangentMode.Broken) newMode = TangentMode.Mirrored; knot.SetTangentMode(newMode, (BezierTangent)tangent.TangentIndex); UpdateHandleRotation(); // Ensures the tangent mode indicators refresh SceneView.RepaintAll(); } } } } [Shortcut("Splines/Cycle Tangent Mode", typeof(SceneView), KeyCode.C)] static void ShortcutCycleTangentMode(ShortcutArguments args) { if (activeTool != null) CycleTangentMode(); } [Shortcut("Splines/Toggle Manipulation Space", typeof(SceneView), KeyCode.X)] static void ShortcutCycleHandleOrientation(ShortcutArguments args) { /* We're doing a switch here (instead of handleOrientation+1 and wrapping) because HandleOrientation.Global/Local values map to PivotRotation.Global/Local (as they should), but PivotRotation.Global = 1 when it's actually the first option and PivotRotation.Local = 0 when it's the second option. */ switch (handleOrientation) { case HandleOrientation.Element: handleOrientation = HandleOrientation.Global; break; case HandleOrientation.Global: handleOrientation = HandleOrientation.Local; break; case HandleOrientation.Local: handleOrientation = HandleOrientation.Parent; break; case HandleOrientation.Parent: handleOrientation = HandleOrientation.Element; break; default: Debug.LogError($"{handleOrientation} handle orientation not supported!"); break; } } } }