using System; using System.Collections.Generic; using Unity.Mathematics; using UnityEngine; using UnityEngine.Splines; using Object = UnityEngine.Object; namespace UnityEditor.Splines { static class TransformOperation { [Flags] public enum PivotFreeze { None = 0, Position = 1, Rotation = 2, All = Position | Rotation } struct TransformData { internal float3 position; internal float3 inTangentDirection; internal float3 outTangentDirection; internal static TransformData GetData(ISplineElement element) { var tData = new TransformData(); tData.position = new float3(element.position); if (element is BezierEditableKnot knot) { tData.inTangentDirection = knot.tangentIn.direction; tData.outTangentDirection = knot.tangentOut.direction; } return tData; } } struct RotationSyncData { quaternion m_RotationDelta; float m_MagnitudeDelta; float m_ScaleMultiplier; // Only used for scale operation bool m_Initialized; public bool initialized => m_Initialized; public quaternion rotationDelta => m_RotationDelta; public float magnitudeDelta => m_MagnitudeDelta; public float scaleMultiplier => m_ScaleMultiplier; public void Initialize(quaternion rotationDelta, float magnitudeDelta, float scaleMultiplier) { m_RotationDelta = rotationDelta; m_MagnitudeDelta = magnitudeDelta; m_ScaleMultiplier = scaleMultiplier; m_Initialized = true; } public void Clear() { m_RotationDelta = quaternion.identity; m_MagnitudeDelta = 0f; m_ScaleMultiplier = 1f; m_Initialized = false; } } static List s_ElementSelection = new List(32); public static IReadOnlyList elementSelection => s_ElementSelection; static int s_ElementSelectionCount = 0; public static bool canManipulate => s_ElementSelectionCount > 0; public static ISplineElement currentElementSelected => canManipulate ? s_ElementSelection[0] : null; static Vector3 s_PivotPosition; public static Vector3 pivotPosition => s_PivotPosition; static quaternion s_HandleRotation; public static quaternion handleRotation => s_HandleRotation; //Caching rotation inverse for rotate and scale operations static quaternion s_HandleRotationInv; public static PivotFreeze pivotFreeze { get; set; } static TransformData[] s_MouseDownData; // Used to prevent same knot being rotated multiple times during a transform operation in Rotation Sync mode. static HashSet s_RotatedKnotCache = new HashSet(); static RotationSyncData s_RotationSyncData = new RotationSyncData(); internal static void UpdateSelection(IEnumerable selection) { SplineSelection.GetSelectedElements(selection, s_ElementSelection); s_ElementSelectionCount = s_ElementSelection.Count; if (s_ElementSelectionCount > 0) { UpdatePivotPosition(); UpdateHandleRotation(); } } internal static void UpdatePivotPosition(bool useKnotPositionForTangents = false) { if ((pivotFreeze & PivotFreeze.Position) != 0) return; switch (Tools.pivotMode) { case PivotMode.Center: s_PivotPosition = EditableSplineUtility.GetBounds(s_ElementSelection, useKnotPositionForTangents).center; break; case PivotMode.Pivot: if (s_ElementSelectionCount == 0) goto default; var element = s_ElementSelection[0]; if (useKnotPositionForTangents && element is EditableTangent tangent) s_PivotPosition = tangent.owner.position; else s_PivotPosition = element.position; break; default: s_PivotPosition = Vector3.positiveInfinity; break; } } // A way to set pivot position for situations, when by design, pivot position does // not necessarily match the pivot of selected elements. internal static void ForcePivotPosition(float3 position) { s_PivotPosition = position; } internal static void UpdateHandleRotation() { if ((pivotFreeze & PivotFreeze.Rotation) != 0) return; var handleRotation = Tools.handleRotation; if (canManipulate && (SplineTool.handleOrientation == HandleOrientation.Element || SplineTool.handleOrientation == HandleOrientation.Parent)) { var curElement = TransformOperation.currentElementSelected; if (SplineTool.handleOrientation == HandleOrientation.Element) handleRotation = CalculateElementSpaceHandleRotation(curElement); else if (curElement is EditableTangent editableTangent) handleRotation = CalculateElementSpaceHandleRotation(editableTangent.owner); } s_HandleRotation = handleRotation; s_HandleRotationInv = math.inverse(s_HandleRotation); } public static void ApplyTranslation(Vector3 delta) { s_RotatedKnotCache.Clear(); foreach (var element in s_ElementSelection) { if (element is EditableKnot knot) { knot.position += (float3)delta; if (!s_RotationSyncData.initialized) s_RotationSyncData.Initialize(quaternion.identity, 0f, 1f); } else if (element is EditableTangent tangent) { //Do nothing on the tangent if the knot is also in the selection if (s_ElementSelection.Contains(tangent.owner)) continue; if (tangent.owner is BezierEditableKnot owner) { if (OppositeTangentSelected(tangent)) owner.SetMode(BezierEditableKnot.Mode.Broken); if (owner.mode == BezierEditableKnot.Mode.Broken) tangent.position = tangent.owner.position + tangent.direction + (float3) delta; else { if (s_RotatedKnotCache.Contains(tangent.owner)) continue; if (tangent.owner is BezierEditableKnot tangentOwner) { var targetDirection = tangent.direction + (float3) delta; // Build rotation sync data based on active selection's transformation if (!s_RotationSyncData.initialized) { var rotationDelta = Quaternion.FromToRotation(tangent.direction, targetDirection); var magnitudeDelta = math.length(targetDirection) - math.length(tangent.direction); s_RotationSyncData.Initialize(rotationDelta, magnitudeDelta, 1f); } ApplyTangentRotationSyncTransform(tangent); } } } } } s_RotationSyncData.Clear(); } public static void ApplyRotation(Quaternion deltaRotation, Vector3 rotationCenter) { s_RotatedKnotCache.Clear(); foreach (var element in s_ElementSelection) { if (element is EditableKnot knot) { var knotRotation = knot.rotation; RotateKnot(knot, deltaRotation, rotationCenter); if (!s_RotationSyncData.initialized) s_RotationSyncData.Initialize(math.mul(math.inverse(knotRotation), knot.rotation), 0f, 1f); } else if (element is EditableTangent tangent && !s_ElementSelection.Contains(tangent.owner)) { if (tangent.owner is BezierEditableKnot tangentOwner) { if (tangentOwner.mode == BezierEditableKnot.Mode.Broken) { if (Tools.pivotMode == PivotMode.Pivot) rotationCenter = tangent.owner.position; var mode = tangentOwner.mode; var deltaPos = math.rotate(deltaRotation, tangent.position - (float3)rotationCenter); tangent.position = deltaPos + (float3)rotationCenter; tangentOwner.TangentChanged(tangent, mode); } else { if (s_RotatedKnotCache.Contains(tangent.owner)) continue; deltaRotation.ToAngleAxis(out var deltaRotationAngle, out var deltaRotationAxis); if (math.abs(deltaRotationAngle) > 0f) { if (tangentOwner.mode != BezierEditableKnot.Mode.Broken) { // If we're in center pivotMode and both tangents of the same knot are in selection, enter Broken mode under these conditions: if (Tools.pivotMode == PivotMode.Center && OppositeTangentSelected(tangent)) { var knotToCenter = (float3) rotationCenter - tangentOwner.position; // 1) Rotation center does not match owner knot's position if (!Mathf.Approximately(math.length(knotToCenter), 0f)) { var similarity = Math.Abs(Vector3.Dot(math.normalize(deltaRotationAxis), math.normalize(knotToCenter))); // 2) Both rotation center and knot, are not on rotation delta's axis if (!Mathf.Approximately(similarity, 1f)) tangentOwner.SetMode(BezierEditableKnot.Mode.Broken); } } } // Build rotation sync data based on active selection's transformation if (!s_RotationSyncData.initialized) { if (Tools.pivotMode == PivotMode.Pivot) s_RotationSyncData.Initialize(deltaRotation, 0f, 1f); else { var deltaPos = math.rotate(deltaRotation, tangent.position - (float3) rotationCenter); var knotToRotationCenter = (float3) rotationCenter - tangent.owner.position; var targetDirection = knotToRotationCenter + deltaPos; var tangentNorm = math.normalize(tangent.direction); var axisDotTangent = math.dot(math.normalize(deltaRotationAxis), tangentNorm); var toRotCenterDotTangent = math.length(knotToRotationCenter) > 0f ? math.dot(math.normalize(knotToRotationCenter), tangentNorm) : 1f; quaternion knotRotationDelta; // In center pivotMode, use handle delta only if our handle delta rotation's axis // matches knot's active selection tangent direction and rotation center is on the tangent's axis. // This makes knot roll possible when element selection list only contains one or both tangents of a single knot. if (Mathf.Approximately(math.abs(axisDotTangent), 1f) && Mathf.Approximately(math.abs(toRotCenterDotTangent), 1f)) knotRotationDelta = deltaRotation; else knotRotationDelta = Quaternion.FromToRotation(tangent.direction, targetDirection); var scaleMultiplier = math.length(targetDirection) / math.length(tangent.direction); s_RotationSyncData.Initialize(knotRotationDelta, 0f, scaleMultiplier); } } ApplyTangentRotationSyncTransform(tangent, false); } } } } } s_RotationSyncData.Clear(); } static bool OppositeTangentSelected(EditableTangent tangent) { if (tangent.owner is BezierEditableKnot tangentOwner && tangentOwner.mode != BezierEditableKnot.Mode.Broken) if (tangentOwner.TryGetOppositeTangent(tangent, out var oppositeTangent) && s_ElementSelection.Contains(oppositeTangent)) return true; return false; } static void RotateKnot(EditableKnot knot, quaternion deltaRotation, float3 rotationCenter, bool allowTranslation = true) { var knotInBrokenMode = (knot is BezierEditableKnot bezierKnot && bezierKnot.mode == BezierEditableKnot.Mode.Broken); if (!knotInBrokenMode && s_RotatedKnotCache.Contains(knot)) return; if (allowTranslation && Tools.pivotMode == PivotMode.Center) { var dir = knot.position - rotationCenter; if (SplineTool.handleOrientation == HandleOrientation.Element || SplineTool.handleOrientation == HandleOrientation.Parent) knot.position = math.rotate(deltaRotation, dir) + rotationCenter; else knot.position = math.rotate(s_HandleRotation, math.rotate(deltaRotation, math.rotate(s_HandleRotationInv, dir))) + rotationCenter; } if (SplineTool.handleOrientation == HandleOrientation.Element || SplineTool.handleOrientation == HandleOrientation.Parent) { if (Tools.pivotMode == PivotMode.Center) knot.rotation = math.mul(deltaRotation, knot.rotation); else { var handlePivotModeRot = math.mul(GetCurrentSelectionKnot().rotation, math.inverse(knot.rotation)); knot.rotation = math.mul(math.inverse(handlePivotModeRot), math.mul(deltaRotation, math.mul(handlePivotModeRot, knot.rotation))); } } else knot.rotation = math.mul(s_HandleRotation, math.mul(deltaRotation, math.mul(s_HandleRotationInv, knot.rotation))); s_RotatedKnotCache.Add(knot); } public static void ApplyScale(float3 scale) { s_RotatedKnotCache.Clear(); ISplineElement[] scaledElements = new ISplineElement[s_ElementSelectionCount]; for(int elementIndex = 0; elementIndex 0f) { // If we're in center pivotMode and both tangents of the same knot are in selection if (Tools.pivotMode == PivotMode.Center && OppositeTangentSelected(tangent)) { var knotToCenter = (float3)pivotPosition - tangentOwner.position; // Enter broken mode if scale operation center does not match owner knot's position if (!Mathf.Approximately(math.length(knotToCenter), 0f)) { tangentOwner.SetMode(BezierEditableKnot.Mode.Broken); var similarity = Math.Abs(Vector3.Dot(math.normalize(scaleDelta), math.normalize(knotToCenter))); // If scale center and knot are both on an axis that's orthogonal to scale operation's axis, // mark knot for mode restore so that mirrored/continous modes can be restored if (Mathf.Approximately(similarity, 0f)) restoreMode = true; } } } var index = Array.IndexOf(scaledElements, element); if (index == -1) //element not scaled yet { if (tangentOwner.mode == BezierEditableKnot.Mode.Broken) tangent.position = ScaleTangent(tangent, s_MouseDownData[elementIndex].position, scale); else { // Build rotation sync data based on active selection's transformation if (!s_RotationSyncData.initialized) { var targetDirection = ScaleTangent(tangent, s_MouseDownData[elementIndex].position, scale) - tangent.owner.position; var rotationDelta = Quaternion.FromToRotation(tangent.direction, targetDirection); var scaleMultiplier = math.length(targetDirection) / math.length(tangent.direction); s_RotationSyncData.Initialize(rotationDelta, 0f, scaleMultiplier); } if (tangentOwner.mode == BezierEditableKnot.Mode.Mirrored && s_RotatedKnotCache.Contains(tangentOwner)) continue; ApplyTangentRotationSyncTransform(tangent, false); } if (restoreMode) tangentOwner.SetMode(mode); } } } scaledElements[elementIndex] = element; } s_RotationSyncData.Clear(); } static void ScaleKnot(EditableKnot knot, int dataIndex, float3 scale) { if(Tools.pivotMode == PivotMode.Center) { var deltaPos = math.rotate(s_HandleRotationInv ,s_MouseDownData[dataIndex].position - (float3) pivotPosition); var deltaPosKnot = deltaPos * scale; knot.position = math.rotate(s_HandleRotation, deltaPosKnot) + (float3)pivotPosition; } using(new BezierEditableKnot.TangentSafeEditScope(knot)) { if(knot is BezierEditableKnot bezierKnot) { var tangent = bezierKnot.tangentIn; tangent.direction = math.rotate(s_HandleRotation, math.rotate(s_HandleRotationInv,s_MouseDownData[dataIndex].inTangentDirection) * scale); tangent = bezierKnot.tangentOut; tangent.direction = math.rotate(s_HandleRotation, math.rotate(s_HandleRotationInv,s_MouseDownData[dataIndex].outTangentDirection) * scale); } } } static float3 ScaleTangent(EditableTangent tangent, float3 originalPosition, float3 scale) { var scaleCenter = Tools.pivotMode == PivotMode.Center ? (float3) pivotPosition : tangent.owner.position; var deltaPos = math.rotate(s_HandleRotationInv, originalPosition - scaleCenter) * scale; return math.rotate(s_HandleRotation, deltaPos) + scaleCenter; } static void ApplyTangentRotationSyncTransform(EditableTangent tangent, bool absoluteScale = true) { if (tangent.owner is BezierEditableKnot tangentOwner) { // Apply scale only if tangent is active selection or it's part of multi select and its knot is mirrored if (tangent == currentElementSelected || tangentOwner.mode == BezierEditableKnot.Mode.Mirrored || (!absoluteScale && tangentOwner.mode == BezierEditableKnot.Mode.Continuous)) { if (absoluteScale) tangent.direction += math.normalize(tangent.direction) * s_RotationSyncData.magnitudeDelta; else tangent.direction *= s_RotationSyncData.scaleMultiplier; } } RotateKnot(tangent.owner, s_RotationSyncData.rotationDelta, tangent.owner.position, false); } internal static quaternion CalculateElementSpaceHandleRotation(ISplineElement element) { quaternion handleRotation = quaternion.identity; if (element is EditableTangent editableTangent && editableTangent.owner is BezierEditableKnot tangentKnot) { float3 forward; var knotUp = math.rotate(tangentKnot.rotation, math.up()); if (math.length(editableTangent.direction) > 0) forward = math.normalize(editableTangent.direction); else // Treat zero length tangent same way as when it's parallel to knot's up vector forward = knotUp; float3 right; var dotForwardKnotUp = math.dot(forward, knotUp); if (Mathf.Approximately(math.abs(dotForwardKnotUp), 1f)) right = math.rotate(tangentKnot.rotation, math.right()) * math.sign(dotForwardKnotUp); else right = math.cross(forward, knotUp); handleRotation = quaternion.LookRotationSafe(forward, math.cross(right, forward)); } else if (element is EditableKnot editableKnot) handleRotation = editableKnot.rotation; return handleRotation; } static EditableKnot GetCurrentSelectionKnot() { if (currentElementSelected == null) return null; if (currentElementSelected is EditableTangent tangent) return tangent.owner; if (currentElementSelected is EditableKnot knot) return knot; return null; } public static void RecordMouseDownState() { s_MouseDownData = new TransformData[s_ElementSelectionCount]; for (int i = 0; i < s_ElementSelectionCount; i++) { s_MouseDownData[i] = TransformData.GetData(s_ElementSelection[i]); } } public static void ClearMouseDownState() { s_MouseDownData = null; } public static Bounds GetSelectionBounds(bool useKnotPositionForTangents = false) { return EditableSplineUtility.GetBounds(s_ElementSelection, useKnotPositionForTangents); } } }