initial commit

This commit is contained in:
Jo 2025-01-07 02:06:59 +01:00
parent 6715289efe
commit 788c3389af
37645 changed files with 2526849 additions and 80 deletions

View file

@ -0,0 +1,364 @@
using System.Collections.Generic;
using UnityEditor.EditorTools;
using UnityEditor.ShortcutManagement;
using UnityEngine;
using UnityEngine.Splines;
using Unity.Mathematics;
#if !UNITY_2020_2_OR_NEWER
using ToolManager = UnityEditor.EditorTools.EditorTools;
#endif
namespace UnityEditor.Splines
{
[EditorTool("Place Spline Knots", typeof(ISplineProvider), typeof(SplineToolContext))]
sealed class KnotPlacementTool : SplineTool
{
public override bool gridSnapEnabled => true;
enum State
{
KnotPlacement,
TangentPlacement,
SplineClosure
}
const string k_DistanceAboveSurfacePrefKey = "KnotPlacementTool_DistanceAboveSurface";
static float? s_DistanceAboveSurface;
public override GUIContent toolbarIcon => PathIcons.knotPlacementTool;
internal override SplineHandlesOptions handlesOptions
{
get
{
switch (m_State)
{
case State.SplineClosure:
case State.TangentPlacement:
return SplineHandlesOptions.ShowTangents;
default:
return SplineHandlesOptions.KnotInsert | SplineHandlesOptions.ShowTangents;
}
}
}
readonly List<IEditableSpline> m_Splines = new List<IEditableSpline>();
int m_ActiveSplineIndex;
State m_State;
Vector3 m_LastSurfacePoint;
Vector3 m_LastSurfaceNormal;
Vector3 m_CustomTangentOut;
Plane m_KnotPlane;
int m_StartId;
int m_EndId;
int m_AddKnotId;
int m_ClosingKnotId;
public override void OnActivated()
{
base.OnActivated();
m_State = State.KnotPlacement;
SplineToolContext.UseCustomSplineHandles(true);
Selection.selectionChanged -= OnSelectionChanged;
Undo.undoRedoPerformed -= OnSelectionChanged;
}
public override void OnWillBeDeactivated()
{
base.OnWillBeDeactivated();
SplineToolContext.UseCustomSplineHandles(false);
Selection.selectionChanged -= OnSelectionChanged;
Undo.undoRedoPerformed -= OnSelectionChanged;
}
void OnSelectionChanged()
{
m_ActiveSplineIndex = -1;
}
public override void OnToolGUI(EditorWindow window)
{
Event evt = Event.current;
GetSelectedSplines(m_Splines);
var spline = GetActiveSpline();
m_StartId = GUIUtility.GetControlID(FocusType.Passive);
m_AddKnotId = GUIUtility.GetControlID(FocusType.Passive);
m_ClosingKnotId = GUIUtility.GetControlID(FocusType.Passive);
var nearestControlIsSpline = HandleUtility.nearestControl == m_AddKnotId
|| HandleUtility.nearestControl == m_ClosingKnotId
//If the spline is closed and the nearest control is not one that is define after in the splines
|| spline.closed && (HandleUtility.nearestControl < m_StartId || HandleUtility.nearestControl > m_EndId);
var isMouseInWindow = new Rect(Vector2.zero, window.position.size).Contains(Event.current.mousePosition);
bool canCloseActiveSpline = false;
for(int i = 0; i < m_Splines.Count; ++i)
{
var active = SplineHandles.DrawSplineHandles(
m_Splines[i],
handlesOptions,
i == m_ActiveSplineIndex && nearestControlIsSpline || !isMouseInWindow);
if(active)
canCloseActiveSpline = m_Splines[i] == spline;
}
m_EndId = GUIUtility.GetControlID(FocusType.Passive);
DoClosingKnot(spline, canCloseActiveSpline);
if (!spline.closed)
DoKnotSurfaceAddHandle(m_AddKnotId, spline, isMouseInWindow);
if(spline.closed && evt.type == EventType.MouseMove)
HandleUtility.Repaint();
SplineConversionUtility.ApplyEditableSplinesIfDirty(targets);
if (evt.type == EventType.KeyDown && (evt.keyCode == KeyCode.Escape || evt.keyCode == KeyCode.Return))
{
ClearTangentPlacementData();
ToolManager.SetActiveTool<SplineMoveTool>();
}
}
void ClearTangentPlacementData()
{
m_State = State.KnotPlacement;
m_LastSurfacePoint = Vector3.zero;
m_LastSurfaceNormal = Vector3.zero;
m_CustomTangentOut = Vector3.zero;
}
void DoClosingKnot(IEditableSpline spline, bool active)
{
if (m_State != State.TangentPlacement && spline.knotCount >= 3 && spline.canBeClosed && !spline.closed)
{
if (HandleUtility.nearestControl == m_ClosingKnotId || m_State == State.SplineClosure)
{
Event evt = Event.current;
switch (evt.GetTypeForControl(m_ClosingKnotId))
{
case EventType.Repaint:
if (!Tools.viewToolActive)
{
var firstKnot = spline.GetKnot(0);
if (firstKnot.tangentCount > 1)
{
var tangentOut = firstKnot.GetTangent(1).direction;
DrawPreviewCurveForNewEndKnot(spline, firstKnot.position, tangentOut, m_LastSurfaceNormal, m_ClosingKnotId, true);
}
else
{
var tangentOut = float3.zero;
DrawPreviewCurveForNewEndKnot(spline, firstKnot.position, tangentOut, m_LastSurfaceNormal, m_ClosingKnotId, true);
}
}
break;
case EventType.MouseDown:
m_State = State.SplineClosure;
break;
}
}
EditableKnot knot = spline.GetKnot(0);
if (SplineHandles.ButtonHandle(m_ClosingKnotId, knot, active))
{
EditableSplineUtility.CloseSpline(spline);
m_State = State.KnotPlacement;
}
}
}
void DoKnotSurfaceAddHandle(int controlID, IEditableSpline spline, bool isMouseInWindow)
{
if (spline == null)
return;
Event evt = Event.current;
switch (evt.GetTypeForControl(controlID))
{
case EventType.Layout:
if (!Tools.viewToolActive)
HandleUtility.AddDefaultControl(controlID);
break;
case EventType.Repaint:
if (HandleUtility.nearestControl == controlID && !Tools.viewToolActive)
{
// Draw curve preview if we're placing tangents. Otherwise, draw it only if the cursor's ray is intersecting with a surface.
if(m_State == State.TangentPlacement ||
( m_State == State.KnotPlacement &&
isMouseInWindow &&
SplineHandleUtility.GetPointOnSurfaces(evt.mousePosition, out m_LastSurfacePoint, out m_LastSurfaceNormal) ))
{
//todo enable this after PR lands
//#if UNITY_2022_2_OR_NEWER
//if(EditorSnapSettings.incrementalSnapActive)
// m_LastSurfacePoint = SplineHandleUtility.DoIncrementSnap(m_LastSurfacePoint, spline.GetKnot(spline.knotCount - 1).position);
//#endif
DrawPreviewCurveForNewEndKnot(spline, m_LastSurfacePoint, m_CustomTangentOut, m_LastSurfaceNormal, controlID);
}
}
break;
case EventType.MouseDown:
if (HandleUtility.nearestControl == controlID && GUIUtility.hotControl == 0 && evt.button == 0)
{
GUIUtility.hotControl = controlID;
evt.Use();
if (SplineHandleUtility.GetPointOnSurfaces(evt.mousePosition, out m_LastSurfacePoint, out m_LastSurfaceNormal))
{
//todo enable this after PR lands
//#if UNITY_2022_2_OR_NEWER
// if(EditorSnapSettings.incrementalSnapActive)
// m_LastSurfacePoint = SplineHandleUtility.DoIncrementSnap(m_LastSurfacePoint, spline.GetKnot(spline.knotCount - 1).position);
//#endif
m_KnotPlane = new Plane(m_LastSurfaceNormal, m_LastSurfacePoint);
if (spline.tangentsPerKnot > 0)
m_State = State.TangentPlacement;
}
}
break;
case EventType.MouseUp:
if (GUIUtility.hotControl == controlID)
{
evt.Use();
if (evt.button == 0)
{
GUIUtility.hotControl = 0;
if (SplineHandleUtility.GetPointOnSurfaces(evt.mousePosition, out Vector3 _, out Vector3 _))
{
m_LastSurfacePoint = SplineHandleUtility.RoundBasedOnMinimumDifference(m_LastSurfacePoint);
//todo enable this after PR lands
//#if UNITY_2022_2_OR_NEWER
// if(EditorSnapSettings.incrementalSnapActive)
// m_LastSurfacePoint = SplineHandleUtility.DoIncrementSnap(m_LastSurfacePoint, spline.GetKnot(spline.knotCount - 1).position);
//#endif
// Check component count to ensure that we only move the transform of a newly created
// spline. I.e., we don't want to move a GameObject that has other components like
// a MeshRenderer, for example.
if (spline.knotCount < 1
&& spline.conversionTarget is Component component
&& component.gameObject.GetComponents<Component>().Length == 2)
component.transform.position = m_LastSurfacePoint;
EditableSplineUtility.AddPointToEnd(spline, m_LastSurfacePoint, m_LastSurfaceNormal, m_CustomTangentOut);
}
ClearTangentPlacementData();
}
}
break;
case EventType.MouseMove:
if (HandleUtility.nearestControl == controlID)
HandleUtility.Repaint();
break;
case EventType.MouseDrag:
if (m_State == State.TangentPlacement && GUIUtility.hotControl == controlID && evt.button == 0)
{
evt.Use();
var ray = HandleUtility.GUIPointToWorldRay(evt.mousePosition);
if (m_KnotPlane.Raycast(ray, out float distance))
m_CustomTangentOut = (ray.origin + ray.direction * distance) - m_LastSurfacePoint;
}
break;
}
}
void DrawPreviewCurveForNewEndKnot(IEditableSpline spline, float3 point, float3 tangentOut, float3 normal, int knotHandleId, bool isClosingCurve = false)
{
var previewCurve = spline.GetPreviewCurveForEndKnot(point, normal, tangentOut);
if (spline.knotCount > 0)
SplineHandles.CurveHandleCap(previewCurve, -1, EventType.Repaint, m_State != State.TangentPlacement);
if (!isClosingCurve)
{
for(int i = 0; i < previewCurve.b.tangentCount; ++i)
{
var tangent = previewCurve.b.GetTangent(i);
if(math.length(tangent.localPosition) > 0)
SplineHandles.DrawTangentHandle(tangent);
}
// In addition, display the normally hidden tangent out of the last knot.
// It gives an impression of an issue when it's hidden but the preview knot shows both tangents.
if (spline.knotCount > 0 && previewCurve.a.tangentCount > 0)
SplineHandles.DrawTangentHandle(previewCurve.a.GetTangent(previewCurve.a.tangentCount - 1));
SplineHandles.DrawKnotHandle(m_LastSurfacePoint, previewCurve.b.rotation, false, knotHandleId);
}
else
{
var firstKnot = spline.GetKnot(0);
if (firstKnot.tangentCount > 1)
{
SplineHandles.DrawTangentHandle(firstKnot.GetTangent(0));
if (spline.knotCount > 0 && previewCurve.a.tangentCount > 0)
SplineHandles.DrawTangentHandle(previewCurve.a.GetTangent(previewCurve.a.tangentCount - 1));
}
}
}
void GetSelectedSplines(List<IEditableSpline> results)
{
results.Clear();
foreach (var t in targets)
{
IReadOnlyList<IEditableSpline> paths = EditableSplineManager.GetEditableSplines(t);
if (paths == null)
continue;
for (int i = 0; i < paths.Count; ++i)
{
results.AddRange(paths);
}
}
}
void CycleActiveSpline()
{
m_ActiveSplineIndex = (m_ActiveSplineIndex + 1) % m_Splines.Count;
SceneView.RepaintAll();
}
protected override IEditableSpline GetActiveSpline()
{
IEditableSpline spline;
if(m_ActiveSplineIndex == -1)
{
spline = base.GetActiveSpline();
m_ActiveSplineIndex = m_Splines.IndexOf(spline);
}
else
spline = m_Splines[m_ActiveSplineIndex];
return spline;
}
[Shortcut("Splines/Cycle Active Spline", typeof(SceneView), KeyCode.S)]
static void ShortcutCycleActiveSpline(ShortcutArguments args)
{
if(m_ActiveTool is KnotPlacementTool tool)
tool.CycleActiveSpline();
}
}
}

View file

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

View file

@ -0,0 +1,69 @@
using System.Collections.Generic;
using UnityEditor.EditorTools;
using UnityEditor.Overlays;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
[CustomEditor(typeof(SplineMoveTool))]
class SplineMoveToolSettings : SplineToolSettings { }
#if UNITY_2021_2_OR_NEWER
[EditorTool("Spline Move Tool", typeof(ISplineProvider), typeof(SplineToolContext))]
#else
[EditorTool("Spline Move Tool", typeof(ISplineProvider))]
#endif
sealed class SplineMoveTool : SplineTool
{
public override bool gridSnapEnabled
{
get => handleOrientation == HandleOrientation.Global;
}
public override GUIContent toolbarIcon => PathIcons.splineMoveTool;
internal override SplineHandlesOptions handlesOptions => SplineHandlesOptions.ManipulationDefault;
public override void OnToolGUI(EditorWindow window)
{
if (Event.current.type == EventType.MouseDrag)
{
if (handleOrientation == HandleOrientation.Element || handleOrientation == HandleOrientation.Parent)
TransformOperation.pivotFreeze |= TransformOperation.PivotFreeze.Rotation;
// In rotation sync center mode, pivot has to be allowed to move away
// from the selection center. Therefore we freeze pivot's position
// and force the position later on based on handle's translation delta.
if (Tools.pivotMode == PivotMode.Center)
TransformOperation.pivotFreeze |= TransformOperation.PivotFreeze.Position;
}
if (Event.current.type == EventType.MouseUp)
{
TransformOperation.pivotFreeze = TransformOperation.PivotFreeze.None;
TransformOperation.UpdatePivotPosition();
TransformOperation.UpdateHandleRotation();
}
if (Event.current.type == EventType.Layout)
TransformOperation.UpdatePivotPosition();
if (TransformOperation.canManipulate)
{
EditorGUI.BeginChangeCheck();
var newPos = Handles.DoPositionHandle(TransformOperation.pivotPosition, TransformOperation.handleRotation);
if (EditorGUI.EndChangeCheck())
{
TransformOperation.ApplyTranslation(newPos - TransformOperation.pivotPosition);
if (Tools.pivotMode == PivotMode.Center)
TransformOperation.ForcePivotPosition(newPos);
}
}
SplineConversionUtility.ApplyEditableSplinesIfDirty(targets);
}
}
}

View file

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

View file

@ -0,0 +1,62 @@
using UnityEngine;
using UnityEditor.EditorTools;
using UnityEditor.Overlays;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
[CustomEditor(typeof(SplineRotateTool))]
class SplineRotateToolSettings : SplineToolSettings { }
#if UNITY_2021_2_OR_NEWER
[EditorTool("Spline Rotate", typeof(ISplineProvider), typeof(SplineToolContext))]
#else
[EditorTool("Spline Rotate", typeof(ISplineProvider))]
#endif
sealed class SplineRotateTool : SplineTool
{
public override GUIContent toolbarIcon => PathIcons.splineRotateTool;
internal override SplineHandlesOptions handlesOptions => SplineHandlesOptions.ManipulationDefault;
Quaternion m_CurrentRotation = Quaternion.identity;
Vector3 m_RotationCenter = Vector3.zero;
public override void OnToolGUI(EditorWindow window)
{
if (Event.current.type == EventType.MouseDrag)
{
if (handleOrientation == HandleOrientation.Element || handleOrientation == HandleOrientation.Parent)
TransformOperation.pivotFreeze |= TransformOperation.PivotFreeze.Rotation;
}
if (Event.current.type == EventType.MouseUp)
{
TransformOperation.pivotFreeze = TransformOperation.PivotFreeze.None;
TransformOperation.UpdateHandleRotation();
}
if (Event.current.type == EventType.Layout)
TransformOperation.UpdatePivotPosition(true);
if(TransformOperation.canManipulate)
{
EditorGUI.BeginChangeCheck();
var rotation = Handles.DoRotationHandle(m_CurrentRotation, m_RotationCenter);
if(EditorGUI.EndChangeCheck())
{
TransformOperation.ApplyRotation(rotation * Quaternion.Inverse(m_CurrentRotation), m_RotationCenter);
m_CurrentRotation = rotation;
}
if(GUIUtility.hotControl == 0)
{
TransformOperation.UpdateHandleRotation();
m_CurrentRotation = TransformOperation.handleRotation;
m_RotationCenter = TransformOperation.pivotPosition;
}
}
SplineConversionUtility.ApplyEditableSplinesIfDirty(targets);
}
}
}

View file

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

View file

@ -0,0 +1,59 @@
using UnityEditor.EditorTools;
using UnityEditor.Overlays;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
[CustomEditor(typeof(SplineScaleTool))]
class SplineScaleToolSettings : SplineToolSettings { }
#if UNITY_2021_2_OR_NEWER
[EditorTool("Spline Scale Tool", typeof(ISplineProvider), typeof(SplineToolContext))]
#else
[EditorTool("Spline Scale Tool", typeof(ISplineProvider))]
#endif
sealed class SplineScaleTool : SplineTool
{
public override GUIContent toolbarIcon => PathIcons.splineScaleTool;
internal override SplineHandlesOptions handlesOptions => SplineHandlesOptions.ManipulationDefault;
Vector3 m_currentScale = Vector3.one;
public override void OnToolGUI(EditorWindow window)
{
if (Event.current.type == EventType.MouseDrag)
{
if (handleOrientation == HandleOrientation.Element || handleOrientation == HandleOrientation.Parent)
TransformOperation.pivotFreeze |= TransformOperation.PivotFreeze.Rotation;
}
if (Event.current.type == EventType.Layout)
TransformOperation.UpdatePivotPosition(true);
if (Event.current.type == EventType.MouseDown)
{
TransformOperation.RecordMouseDownState();
TransformOperation.pivotFreeze = TransformOperation.PivotFreeze.Position;
}
if(Event.current.type == EventType.MouseUp)
{
m_currentScale = Vector3.one;
TransformOperation.ClearMouseDownState();
TransformOperation.pivotFreeze = TransformOperation.PivotFreeze.None;
TransformOperation.UpdateHandleRotation();
}
if(TransformOperation.canManipulate)
{
EditorGUI.BeginChangeCheck();
m_currentScale = Handles.DoScaleHandle(m_currentScale, TransformOperation.pivotPosition,
TransformOperation.handleRotation, HandleUtility.GetHandleSize(TransformOperation.pivotPosition));
if(EditorGUI.EndChangeCheck())
TransformOperation.ApplyScale(m_currentScale);
}
SplineConversionUtility.ApplyEditableSplinesIfDirty(targets);
}
}
}

View file

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

View file

@ -0,0 +1,323 @@
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
{
/// <summary>
/// Describes how the handles are oriented.
/// </summary>
enum HandleOrientation
{
/// <summary>
/// Tool handles are in the active object's rotation.
/// </summary>
Local = 0,
/// <summary>
/// Tool handles are in global rotation.
/// </summary>
Global = 1,
/// <summary>
/// Tool handles are in active element's parent's rotation.
/// </summary>
Parent = 2,
/// <summary>
/// Tool handles are in active element's rotation.
/// </summary>
Element = 3
}
#if UNITY_2022_1_OR_NEWER
abstract class SplineToolSettings : UnityEditor.Editor, ICreateToolbar
#else
abstract class SplineToolSettings : UnityEditor.Editor
#endif
{
public virtual IEnumerable<string> toolbarElements
{
get
{
yield return "Tool Settings/Pivot Mode";
yield return "Spline Tool Settings/Handle Rotation";
}
}
#if !UNITY_2022_1_OR_NEWER
const string k_ElementClassName = "unity-editor-toolbar-element";
const string k_StyleSheetsPath = "StyleSheets/Toolbars/";
static VisualElement CreateToolbar()
{
var target = new VisualElement();
var path = k_StyleSheetsPath + "EditorToolbar";
var common = EditorGUIUtility.Load($"{path}Common.uss") as StyleSheet;
if (common != null)
target.styleSheets.Add(common);
var themeSpecificName = EditorGUIUtility.isProSkin ? "Dark" : "Light";
var themeSpecific = EditorGUIUtility.Load($"{path}{themeSpecificName}.uss") as StyleSheet;
if (themeSpecific != null)
target.styleSheets.Add(themeSpecific);
target.AddToClassList("unity-toolbar-overlay");
target.style.flexDirection = FlexDirection.Row;
return target;
}
public override VisualElement CreateInspectorGUI()
{
var root = CreateToolbar();
var elements = TypeCache.GetTypesWithAttribute(typeof(EditorToolbarElementAttribute));
foreach (var element in toolbarElements)
{
var type = elements.FirstOrDefault(x =>
{
var attrib = x.GetCustomAttribute<EditorToolbarElementAttribute>();
return attrib != null && attrib.id == element;
});
if (type != null)
{
try
{
const BindingFlags flags = BindingFlags.Instance |
BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.CreateInstance;
var ve = (VisualElement)Activator.CreateInstance(type, flags, null, null, null, null);
ve.AddToClassList(k_ElementClassName);
root.Add(ve);
}
catch (Exception e)
{
Debug.LogError($"Failed creating toolbar element from ID \"{element}\".\n{e}");
}
}
}
EditorToolbarUtility.SetupChildrenAsButtonStrip(root);
return root;
}
#endif
}
/// <summary>
/// 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.
/// </summary>
abstract class SplineTool : EditorTool
{
internal virtual SplineHandlesOptions handlesOptions => SplineHandlesOptions.None;
static UserSetting<HandleOrientation> m_HandleOrientation = new UserSetting<HandleOrientation>(PathSettings.instance, "SplineTool.HandleOrientation", HandleOrientation.Global, SettingsScope.User);
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;
// Workaround for lack of access to ShortcutContext. Use this to pass shortcut actions to tool instances.
protected static SplineTool m_ActiveTool;
/// <summary>
/// Invoked after this EditorTool becomes the active tool.
/// </summary>
public override void OnActivated()
{
SplineToolContext.SetHandlesOptions(handlesOptions);
SplineSelection.changed += OnSplineSelectionChanged;
Spline.afterSplineWasModified += AfterSplineWasModified;
Undo.undoRedoPerformed += UndoRedoPerformed;
Tools.pivotRotationChanged += OnPivotRotationChanged;
Tools.pivotModeChanged += OnPivotModeChanged;
TransformOperation.UpdateSelection(targets);
handleOrientationChanged += OnHandleOrientationChanged;
m_ActiveTool = this;
}
/// <summary>
/// Invoked before this EditorTool stops being the active tool.
/// </summary>
public override void OnWillBeDeactivated()
{
SplineToolContext.SetHandlesOptions(SplineHandlesOptions.None);
SplineSelection.changed -= OnSplineSelectionChanged;
Spline.afterSplineWasModified -= AfterSplineWasModified;
Undo.undoRedoPerformed -= UndoRedoPerformed;
Tools.pivotRotationChanged -= OnPivotRotationChanged;
Tools.pivotModeChanged -= OnPivotModeChanged;
handleOrientationChanged -= OnHandleOrientationChanged;
m_ActiveTool = null;
}
protected virtual void OnHandleOrientationChanged()
{
TransformOperation.UpdateHandleRotation();
}
static void OnPivotRotationChanged()
{
handleOrientation = (HandleOrientation)Tools.pivotRotation;
}
protected virtual void OnPivotModeChanged()
{
TransformOperation.UpdatePivotPosition();
TransformOperation.UpdateHandleRotation();
}
void AfterSplineWasModified(Spline spline) => UpdateSelection();
void UndoRedoPerformed() => UpdateSelection();
void OnSplineSelectionChanged()
{
TransformOperation.pivotFreeze = TransformOperation.PivotFreeze.None;
TransformOperation.UpdateHandleRotation();
TransformOperation.UpdatePivotPosition();
UpdateSelection();
}
void UpdateSelection()
{
TransformOperation.UpdateSelection(targets);
}
void CycleTangentMode()
{
var elementSelection = TransformOperation.elementSelection;
foreach (var element in elementSelection)
{
if (element is EditableTangent tangent)
{
//Do nothing on the tangent if the knot is also in the selection
if (elementSelection.Contains(tangent.owner))
continue;
var oppositeTangentSelected = false;
if (tangent.owner is BezierEditableKnot owner)
{
if(owner.TryGetOppositeTangent(tangent, out var oppositeTangent))
{
if (elementSelection.Contains(oppositeTangent))
oppositeTangentSelected = true;
}
if (!oppositeTangentSelected)
{
if (owner.mode == BezierEditableKnot.Mode.Broken)
{
// Mirror otherTangent against the active tangent prior to SetMode call.
// As SetMode always mirrors tangentOut against tangentIn, this prevents an active selection's
// tangentOut from shrinking or becoming zero tangent unexpectedly.
for (int i = 0; i < owner.tangentCount; ++i)
{
var otherTangent = owner.GetTangent(i);
if (otherTangent != tangent)
otherTangent.SetLocalPositionNoNotify(-tangent.localPosition);
}
owner.SetMode(BezierEditableKnot.Mode.Mirrored);
}
else if (owner.mode == BezierEditableKnot.Mode.Mirrored)
owner.SetMode(BezierEditableKnot.Mode.Continuous);
else if (owner.mode == BezierEditableKnot.Mode.Continuous)
owner.SetMode(BezierEditableKnot.Mode.Broken);
owner.TangentChanged(tangent, owner.mode);
TransformOperation.UpdateHandleRotation();
// Ensures the tangent mode indicators refresh
SceneView.RepaintAll();
}
}
}
}
}
/// <summary>
/// Get the currently selected active spline.
/// </summary>
/// <returns>The active spline.</returns>
protected virtual IEditableSpline GetActiveSpline()
{
IReadOnlyList<IEditableSpline> paths = EditableSplineUtility.GetSelectedSpline(target);
if (paths == null || paths.Count == 0)
return null;
return paths[0];
}
[Shortcut("Splines/Cycle Tangent Mode", typeof(SceneView), KeyCode.C)]
static void ShortcutCycleTangentMode(ShortcutArguments args)
{
if(m_ActiveTool != null)
m_ActiveTool.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;
}
}
}
}

View file

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

View file

@ -0,0 +1,242 @@
using System;
using System.Collections.Generic;
using UnityEditor.EditorTools;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
[Flags]
enum SplineHandlesOptions
{
ShowTangents = 1 << 0,
SelectableKnots = 1 << 1,
SelectableTangents = 1 << 2,
KnotInsert = 1 << 3,
SelectableElements = SelectableKnots | SelectableTangents,
None = 0,
ManipulationDefault = ShowTangents | SelectableElements
}
/// <summary>
/// Defines a tool context for editing splines. When authoring tools for splines, pass the SplineToolContext type
/// to the EditorToolAttribute.editorToolContext parameter to register as a spline tool.
/// </summary>
#if UNITY_2021_2_OR_NEWER
[EditorToolContext("Spline", typeof(ISplineProvider)), Icon(k_IconPath)]
#else
[EditorToolContext("Spline", typeof(ISplineProvider))]
#endif
public sealed class SplineToolContext : EditorToolContext
{
const string k_IconPath = "Packages/com.unity.splines/Editor/Resources/Icons/SplineContext.png";
static SplineHandlesOptions s_SplineHandlesOptions = SplineHandlesOptions.None;
static bool s_UseCustomSplineHandles = false;
static bool s_IsSplineTool = true;
readonly SplineElementRectSelector m_RectSelector = new SplineElementRectSelector();
readonly List<IEditableSpline> m_Splines = new List<IEditableSpline>();
readonly List<EditableKnot> m_KnotBuffer = new List<EditableKnot>();
bool m_WasActiveAfterDeserialize;
internal static void SetHandlesOptions(SplineHandlesOptions options)
{
s_SplineHandlesOptions = options;
}
internal static void UseCustomSplineHandles(bool useCustomSplineHandle)
{
s_UseCustomSplineHandles = useCustomSplineHandle;
}
/// <summary>
/// Returns the matching EditorTool type for the specified Tool given the context.
/// </summary>
/// <param name="tool">The Tool to resolve to an EditorTool type.</param>
/// <returns> An EditorTool type for the requested Tool.</returns>
protected override Type GetEditorToolType(Tool tool)
{
if (tool == Tool.Move)
return typeof(SplineMoveTool);
if (tool == Tool.Rotate)
return typeof(SplineRotateTool);
if (tool == Tool.Scale)
return typeof(SplineScaleTool);
return null;
}
/// <summary>
/// Invoked for each window where this context is active. The spline context uses this method to implement
/// common functionality for working with splines, ex gizmo drawing and selection.
/// </summary>
/// <param name="window"></param>
public override void OnToolGUI(EditorWindow window)
{
if(!s_IsSplineTool)
return;
m_RectSelector.OnGUI(m_Splines);
if(!s_UseCustomSplineHandles)
SplineHandles.DrawSplineHandles(m_Splines, s_SplineHandlesOptions);
HandleSelectionFraming();
HandleSelectAll();
HandleDeleteSelectedKnots();
}
void OnEnable()
{
AssemblyReloadEvents.afterAssemblyReload += OnAfterDomainReload;
}
/// <summary>
/// Invoked after this EditorToolContext becomes the active tool context.
/// </summary>
public override void OnActivated()
{
// Sync handleOrientation to Tools.pivotRotation only if we're switching from a different context.
// This ensures that Parent/Element handleOrientation is retained after domain reload.
if(!m_WasActiveAfterDeserialize)
SplineTool.handleOrientation = (HandleOrientation)Tools.pivotRotation;
else
m_WasActiveAfterDeserialize = false;
OnSelectionChanged();
ToolManager.activeToolChanged += OnActiveToolChanged;
Selection.selectionChanged += OnSelectionChanged;
Spline.afterSplineWasModified += OnSplineWasModified;
Undo.undoRedoPerformed += UndoRedoPerformed;
SplineConversionUtility.splinesUpdated += SceneView.RepaintAll;
SplineConversionUtility.UpdateEditableSplinesForTargets(targets);
}
/// <summary>
/// Invoked before this EditorToolContext stops being the active tool context.
/// </summary>
public override void OnWillBeDeactivated()
{
ToolManager.activeToolChanged -= OnActiveToolChanged;
Selection.selectionChanged -= OnSelectionChanged;
Spline.afterSplineWasModified -= OnSplineWasModified;
Undo.undoRedoPerformed -= UndoRedoPerformed;
SplineConversionUtility.splinesUpdated -= SceneView.RepaintAll;
EditableSplineManager.FreeEntireCache();
SplineSelection.ClearNoUndo(false);
}
void OnActiveToolChanged()
{
if(ToolManager.activeToolType == typeof(KnotPlacementTool))
SplineSelection.Clear();
s_IsSplineTool = typeof(SplineTool).IsAssignableFrom(ToolManager.activeToolType);
}
void OnSplineWasModified(Spline spline) => UpdateSelection();
void OnSelectionChanged() => UpdateSelection();
void UndoRedoPerformed() => UpdateSelection();
void UpdateSelection()
{
EditableSplineUtility.GetSelectedSplines(targets, m_Splines);
EditableSplineManager.UpdateSelection(targets);
SplineSelection.UpdateObjectSelection(targets);
SceneView.RepaintAll();
}
void HandleDeleteSelectedKnots()
{
Event evt = Event.current;
if (evt.type == EventType.KeyDown && evt.keyCode == KeyCode.Delete)
{
SplineSelection.GetSelectedKnots(m_KnotBuffer);
//Sort knots index so removing them doesn't cause the rest of the indices to be invalid
m_KnotBuffer.Sort((a, b) => a.index.CompareTo(b.index));
for (int i = m_KnotBuffer.Count - 1; i >= 0; --i)
{
EditableKnot knot = m_KnotBuffer[i];
knot.spline.RemoveKnotAt(knot.index);
}
evt.Use();
}
}
void HandleSelectionFraming()
{
if (TransformOperation.canManipulate)
{
Event evt = Event.current;
if (evt.commandName.Equals("FrameSelected"))
{
var execute = evt.type == EventType.ExecuteCommand;
if (evt.type == EventType.ValidateCommand || execute)
{
if (execute)
{
var selectionBounds = TransformOperation.GetSelectionBounds(false);
selectionBounds.Encapsulate(TransformOperation.pivotPosition);
var size = selectionBounds.size;
if (selectionBounds.size.x < 1f)
size.x = 1f;
if (selectionBounds.size.y < 1f)
size.y = 1f;
if (selectionBounds.size.z < 1f)
size.z = 1f;
selectionBounds.size = size;
SceneView.lastActiveSceneView.Frame(selectionBounds, false);
}
evt.Use();
}
}
}
}
void HandleSelectAll()
{
Event evt = Event.current;
if (evt.commandName.Equals("SelectAll"))
{
var execute = evt.type == EventType.ExecuteCommand;
if (evt.type == EventType.ValidateCommand || execute)
{
var allElements = new List<ISplineElement>();
foreach (var spline in m_Splines)
{
for (int knotIdx = 0; knotIdx < spline.knotCount; ++knotIdx)
{
var knot = spline.GetKnot(knotIdx);
allElements.Add(knot);
for (int tangentIdx = 0; tangentIdx < knot.tangentCount; ++tangentIdx)
{
var tangent = knot.GetTangent(tangentIdx);
if (SplineSelectionUtility.IsSelectable(spline, knotIdx, tangent))
allElements.Add(tangent);
}
}
}
SplineSelection.Add(allElements);
evt.Use();
}
}
}
void OnAfterDomainReload()
{
m_WasActiveAfterDeserialize = ToolManager.activeContextType == typeof(SplineToolContext);
AssemblyReloadEvents.afterAssemblyReload -= OnAfterDomainReload;
}
}
}

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: da047b2433894bf182b9e42abb971f5f
timeCreated: 1618944106

View file

@ -0,0 +1,535 @@
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<ISplineElement> s_ElementSelection = new List<ISplineElement>(32);
public static IReadOnlyList<ISplineElement> 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<EditableKnot> s_RotatedKnotCache = new HashSet<EditableKnot>();
static RotationSyncData s_RotationSyncData = new RotationSyncData();
internal static void UpdateSelection(IEnumerable<Object> 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<s_ElementSelectionCount; elementIndex++)
{
var element = s_ElementSelection[elementIndex];
if (element is EditableKnot knot)
{
ScaleKnot(knot, elementIndex, scale);
if (!s_RotationSyncData.initialized)
s_RotationSyncData.Initialize(quaternion.identity, 0f, 1f);
}
else if(element is EditableTangent tangent && !s_ElementSelection.Contains(tangent.owner))
{
if(tangent.owner is BezierEditableKnot tangentOwner)
{
var restoreMode = false;
var mode = tangentOwner.mode;
var scaleDelta = scale - new float3(1f, 1f, 1f);
if (tangentOwner.mode != BezierEditableKnot.Mode.Broken && math.length(scaleDelta) > 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);
}
}
}

View file

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