first commit

This commit is contained in:
lethanhsonvsp
2025-11-17 15:16:36 +07:00
commit a40d0921eb
17012 changed files with 2652386 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
using UnityEngine;
using UnityEditor.EditorTools;
using UnityEngine.Splines;
using Unity.Mathematics;
using System.Collections.Generic;
using System;
using UnityEditor.Overlays;
using Object = UnityEngine.Object;
namespace UnityEditor.Splines
{
[CustomEditor(typeof(CreateSplineTool))]
#if UNITY_2022_1_OR_NEWER
class CreateSplineToolSettings : UnityEditor.Editor, ICreateToolbar
{
public IEnumerable<string> toolbarElements
{
#else
class CreateSplineToolSettings : CreateToolbarBase
{
protected override IEnumerable<string> toolbarElements
{
#endif
get { yield return "Spline Tool Settings/Default Knot Type"; }
}
}
#if UNITY_2023_1_OR_NEWER
[EditorTool("Create Spline", toolPriority = 10)]
#else
[EditorTool("Create Spline")]
#endif
[Icon("Packages/com.unity.splines/Editor/Resources/Icons/KnotPlacementTool.png")]
class CreateSplineTool : KnotPlacementTool
{
[NonSerialized]
List<Object> m_Targets = new List<Object>(1);
protected override void AddKnotOnSurface(float3 position, float3 normal, float3 tangentOut)
{
if (MainTarget == null)
{
var gameObject = SplineMenu.CreateSplineGameObject(new MenuCommand(null));
gameObject.transform.localPosition = Vector3.zero;
gameObject.transform.localRotation = Quaternion.identity;
MainTarget = gameObject.GetComponent<SplineContainer>();
Selection.activeGameObject = gameObject;
EditorSplineGizmos.showSelectedGizmo = false;
// Set hasChanged to false as we don't want to override a custom transform set by the user.
gameObject.transform.hasChanged = false;
}
base.AddKnotOnSurface(position, normal, tangentOut);
}
public override void OnActivated()
{
base.OnActivated();
// Enable the gizmo drawing of the selected object because we aren't drawing using handles
EditorSplineGizmos.showSelectedGizmo = true;
}
public override void OnWillBeDeactivated()
{
EditorSplineGizmos.showSelectedGizmo = false;
base.OnWillBeDeactivated();
}
void UpdateTargets()
{
m_Targets.Clear();
if (ToolManager.activeContextType == typeof(SplineToolContext))
{
m_Targets.AddRange(SplineToolContext.GetTargets());
MainTarget = m_Targets[0] as Component;
}
else if (MainTarget != null)
m_Targets.Add(MainTarget);
}
protected override IEnumerable<Object> GetTargets()
{
UpdateTargets();
return m_Targets;
}
protected override IReadOnlyList<Object> GetSortedTargets(out Object mainTarget)
{
UpdateTargets();
mainTarget = MainTarget;
return m_Targets;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 54e2c746318aaa54bb759b9c840a9b24

View File

@@ -0,0 +1,78 @@
#if !UNITY_2022_1_OR_NEWER
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor.Toolbars;
using UnityEngine;
using UnityEngine.UIElements;
namespace UnityEditor.Splines
{
abstract class CreateToolbarBase : UnityEditor.Editor
{
protected abstract IEnumerable<string> toolbarElements { get; }
const string k_ElementClassName = "unity-editor-toolbar-element";
const string k_StyleSheetsPath = "StyleSheets/Toolbars/";
protected 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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4d8b62347ec04b2ea5a72d11f9f428eb
timeCreated: 1649862677

View File

@@ -0,0 +1,64 @@
using Unity.Mathematics;
using UnityEngine;
namespace UnityEditor.Splines
{
class CurvePlacementData : PlacementData
{
readonly SplineCurveHit m_Hit;
public CurvePlacementData(Vector2 mouse, SplineCurveHit hit) : base(mouse, hit.Position, hit.Normal, hit.NextKnot.SplineInfo.Transform.lossyScale)
{
m_Hit = hit;
}
public override SelectableKnot GetOrCreateLinkedKnot()
{
EditorSplineUtility.RecordObject(m_Hit.NextKnot.SplineInfo, "Insert Knot");
return EditorSplineUtility.InsertKnot(m_Hit.NextKnot.SplineInfo, m_Hit.NextKnot.KnotIndex, m_Hit.T);
}
}
class KnotPlacementData : PlacementData
{
readonly SelectableKnot m_Target;
public KnotPlacementData(Vector3 mouse, SelectableKnot target) : base(mouse, target.Position, math.mul(target.Rotation, math.up()), target.SplineInfo.Transform.lossyScale)
{
m_Target = target;
}
public override SelectableKnot GetOrCreateLinkedKnot()
{
return m_Target;
}
}
class PlacementData
{
public Vector2 MousePosition { get; }
public Vector3 TangentOut { get; set; }
public Vector3 Position { get; }
public Vector3 Normal { get; }
public Vector3 Scale { get; }
public Plane Plane { get; }
public PlacementData(Vector2 mouse, Vector3 position, Vector3 normal)
{
MousePosition = mouse;
Position = position;
Normal = normal;
Scale = Vector3.one;
TangentOut = Vector3.zero;
Plane = new Plane(normal, position);
}
public PlacementData(Vector2 mouse, Vector3 position, Vector3 normal, Vector3 scale) : this(mouse, position, normal)
{
Scale = scale;
}
public virtual SelectableKnot GetOrCreateLinkedKnot() => default;
}
}

View File

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

View File

@@ -0,0 +1,813 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.EditorTools;
using UnityEngine;
using UnityEngine.Splines;
using Unity.Mathematics;
using UnityEditor.SettingsManagement;
using UnityEditor.ShortcutManagement;
using Object = UnityEngine.Object;
#if UNITY_2022_1_OR_NEWER
using UnityEditor.Overlays;
#endif
namespace UnityEditor.Splines
{
abstract class KnotPlacementTool : SplineTool
{
// 6f is the threshold used in RectSelection, but felt a little too sensitive when drawing a path.
const float k_MinDragThreshold = 8f;
[UserSetting("Knot Placement", "Drag to Set Tangent Length", "When placing new knots, click then drag to adjust" +
" the length and direction of the tangents. Disable this option to always place auto-smooth knots.")]
static Pref<bool> s_EnableDragTangent = new ($"{nameof(KnotPlacementHandle)}.{nameof(s_EnableDragTangent)}", true);
static readonly string k_KnotPlacementUndoMessage = L10n.Tr("Add Spline Knot");
sealed class DrawingOperation : IDisposable
{
/// <summary>
/// Indicates whether the knot placed is at the start or end of a curve segment
/// </summary>
public enum DrawingDirection
{
Start,
End
}
public bool HasStartedDrawing { get; private set; }
public DrawingDirection Direction
{
get => m_Direction;
}
public readonly SplineInfo CurrentSplineInfo;
readonly DrawingDirection m_Direction;
readonly bool m_AllowDeleteIfNoCurves;
/// <summary>
/// Gets the last index of the knot on the spline
/// </summary>
/// <returns>The index of the last knot on the spline - this will be the same as the starting knot
/// in a closed spline</returns>
int GetLastKnotIndex()
{
var isFromStartAndClosed = m_Direction == DrawingDirection.Start && CurrentSplineInfo.Spline.Closed;
var isFromEndAndOpened = m_Direction == DrawingDirection.End && !CurrentSplineInfo.Spline.Closed;
return isFromStartAndClosed || isFromEndAndOpened ? ( CurrentSplineInfo.Spline.Count - 1 ) : 0;
}
internal SelectableKnot GetLastAddedKnot()
{
return new SelectableKnot(CurrentSplineInfo, GetLastKnotIndex());
}
public DrawingOperation(SplineInfo splineInfo, DrawingDirection direction, bool allowDeleteIfNoCurves)
{
CurrentSplineInfo = splineInfo;
m_Direction = direction;
m_AllowDeleteIfNoCurves = allowDeleteIfNoCurves;
}
public void OnGUI(IReadOnlyList<SplineInfo> splines)
{
KnotPlacementHandle(splines, this, CreateKnotOnKnot, CreateKnotOnSurface, DrawCurvePreview);
}
void UncloseSplineIfNeeded()
{
// If the spline was closed, we unclose it, create a knot on the last knot and connect the first and last
if (CurrentSplineInfo.Spline.Closed)
{
CurrentSplineInfo.Spline.Closed = false;
switch (m_Direction)
{
case DrawingDirection.Start:
{
var lastKnot = new SelectableKnot(CurrentSplineInfo, CurrentSplineInfo.Spline.Count - 1);
var normal = math.mul(lastKnot.Rotation, math.up());
EditorSplineUtility.AddKnotToTheStart(CurrentSplineInfo, lastKnot.Position, normal,
-lastKnot.TangentOut.Direction);
EditorSplineUtility.LinkKnots(new SelectableKnot(CurrentSplineInfo, 0),
new SelectableKnot(CurrentSplineInfo, CurrentSplineInfo.Spline.Count - 1));
break;
}
case DrawingDirection.End:
{
var firstKnot = new SelectableKnot(CurrentSplineInfo, 0);
var normal = math.mul(firstKnot.Rotation, math.up());
EditorSplineUtility.AddKnotToTheEnd(CurrentSplineInfo, firstKnot.Position, normal,
-firstKnot.TangentIn.Direction);
EditorSplineUtility.LinkKnots(new SelectableKnot(CurrentSplineInfo, 0),
new SelectableKnot(CurrentSplineInfo, CurrentSplineInfo.Spline.Count - 1));
break;
}
}
}
}
internal void CreateKnotOnKnot(SelectableKnot knot, float3 tangentOut)
{
EditorSplineUtility.RecordObject(CurrentSplineInfo, k_KnotPlacementUndoMessage);
var lastAddedKnot = GetLastAddedKnot();
// The knot parameter of this method, in a practical context is the knot the user has clicked on. It is
// NOT necessarily the closing knot of this spline.
// If the user clicks on the closing knot (or a knot linked to the closing knot) of the spline, close the spline.
var closeKnotIndex = m_Direction == DrawingDirection.End ? 0 : knot.SplineInfo.Spline.Count - 1;
if (knot.SplineInfo.Equals(CurrentSplineInfo)
&& ( knot.KnotIndex == closeKnotIndex ||
EditorSplineUtility.AreKnotLinked(knot,
new SelectableKnot(CurrentSplineInfo, closeKnotIndex)) ))
{
knot.SplineInfo.Spline.Closed = true;
bool didDrawTangent = math.lengthsq(tangentOut) > float.Epsilon;
// Closing a spline should only affect the closing knot (first or last knot),
// not the clicked knot. If we drew a tangent or the closing knot is Auto tangent
// mode, we set closing knot tangent mode to Broken to retain the shape.
var closingKnot = new SelectableKnot(CurrentSplineInfo, closeKnotIndex);
if (didDrawTangent || closingKnot.Mode == TangentMode.AutoSmooth)
closingKnot.Mode = TangentMode.Broken;
if (didDrawTangent)
{
SelectableTangent tangent;
switch (m_Direction)
{
case DrawingDirection.Start:
tangent = new SelectableTangent(knot.SplineInfo, closeKnotIndex, BezierTangent.Out);
break;
case DrawingDirection.End:
tangent = new SelectableTangent(knot.SplineInfo, closeKnotIndex, BezierTangent.In);
break;
default:
tangent = default;
break;
}
tangent.Direction = -tangentOut;
}
}
else
{
UncloseSplineIfNeeded();
lastAddedKnot = AddKnot(knot.Position, math.mul(knot.Rotation, math.up()), tangentOut);
if (m_Direction == DrawingDirection.End || knot.SplineInfo.Index != lastAddedKnot.SplineInfo.Index)
EditorSplineUtility.LinkKnots(knot, lastAddedKnot);
else
EditorSplineUtility.LinkKnots(new SelectableKnot(knot.SplineInfo, knot.KnotIndex + 1),
lastAddedKnot);
// Already called in AddKnot but this is not recording the updated Linkedknots in that case
PrefabUtility.RecordPrefabInstancePropertyModifications(knot.SplineInfo.Object);
}
}
internal void CreateKnotOnSurface(float3 position, float3 normal, float3 tangentOut)
{
EditorSplineUtility.RecordObject(CurrentSplineInfo, k_KnotPlacementUndoMessage);
var lastKnot = GetLastAddedKnot();
if (lastKnot.IsValid())
position = ApplyIncrementalSnap(position, lastKnot.Position);
UncloseSplineIfNeeded();
AddKnot(position, normal, tangentOut);
}
SelectableKnot AddKnot(float3 position, float3 normal, float3 tangentOut)
{
switch (m_Direction)
{
case DrawingDirection.Start:
return EditorSplineUtility.AddKnotToTheStart(CurrentSplineInfo, position, normal, tangentOut, false);
case DrawingDirection.End:
return EditorSplineUtility.AddKnotToTheEnd(CurrentSplineInfo, position, normal, tangentOut, false);
}
return default;
}
void DrawCurvePreview(float3 position, float3 normal, float3 tangent, SelectableKnot target)
{
if (!Mathf.Approximately(math.length(tangent), 0))
{
TangentHandles.Draw(position - tangent, position, normal);
TangentHandles.Draw(position + tangent, position, normal);
}
var lastKnot = GetLastAddedKnot();
if (target.IsValid() && target.Equals(lastKnot))
return;
var mode = EditorSplineUtility.GetModeFromPlacementTangent(tangent);
position = ApplyIncrementalSnap(position, lastKnot.Position);
if (mode == TangentMode.AutoSmooth)
tangent = SplineUtility.GetAutoSmoothTangent(lastKnot.Position, position, SplineUtility.CatmullRomTension);
BezierCurve previewCurve = m_Direction == DrawingDirection.Start
? EditorSplineUtility.GetPreviewCurveFromStart(CurrentSplineInfo, lastKnot.KnotIndex, position, tangent, mode)
: EditorSplineUtility.GetPreviewCurveFromEnd(CurrentSplineInfo, lastKnot.KnotIndex, position, tangent, mode);
CurveHandles.Draw(-1, previewCurve);
if (SplineUtility.AreTangentsModifiable(lastKnot.Mode) && CurrentSplineInfo.Spline.Count > 0)
TangentHandles.DrawInformativeTangent(previewCurve.P1, previewCurve.P0);
#if UNITY_2022_2_OR_NEWER
KnotHandles.Draw(position, SplineUtility.GetKnotRotation(s_MainTarget.transform.InverseTransformVector(tangent), normal), Handles.elementColor, false, false);
#else
KnotHandles.Draw(position, SplineUtility.GetKnotRotation(s_MainTarget.transform.InverseTransformVector(tangent), normal), SplineHandleUtility.knotColor, false, false);
#endif
}
/// <summary>
/// Remove drawing action that created no curves and were canceled after being created
/// </summary>
public void Dispose()
{
var spline = CurrentSplineInfo.Spline;
if (m_AllowDeleteIfNoCurves && spline != null && spline.Count == 1)
{
EditorSplineUtility.RecordObject(CurrentSplineInfo, "Removing Empty Spline");
CurrentSplineInfo.Container.RemoveSplineAt(CurrentSplineInfo.Index);
//Force to record changes if part of a prefab instance
PrefabUtility.RecordPrefabInstancePropertyModifications(CurrentSplineInfo.Object);
}
}
}
#if UNITY_2022_2_OR_NEWER
public override bool gridSnapEnabled => true;
#endif
static PlacementData s_PlacementData;
static SplineInfo s_ClosestSpline = default;
int m_ActiveObjectIndex = 0;
readonly List<Object> m_SortedTargets = new List<Object>();
readonly List<SplineInfo> m_SplineBuffer = new List<SplineInfo>(4);
static readonly List<SelectableKnot> s_KnotsBuffer = new List<SelectableKnot>();
DrawingOperation m_CurrentDrawingOperation;
static Component s_MainTarget;
//Needed for Tests
internal static Component MainTarget
{
get => s_MainTarget;
set => s_MainTarget = value;
}
public override void OnActivated()
{
EditorSplineGizmos.showSelectedGizmo = false;
s_MainTarget = null;
base.OnActivated();
SplineToolContext.useCustomSplineHandles = true;
SplineSelection.Clear();
SplineSelection.UpdateObjectSelection(GetTargets());
m_ActiveObjectIndex = 0;
}
public override void OnWillBeDeactivated()
{
base.OnWillBeDeactivated();
SplineToolContext.useCustomSplineHandles = false;
EndDrawingOperation();
}
public override void OnToolGUI(EditorWindow window)
{
var targets = GetSortedTargets(out var mainTarget);
s_MainTarget = mainTarget as Component;
var allSplines = EditorSplineUtility.GetSplinesFromTargetsInternal(targets);
//If the spline being drawn on doesn't exist anymore, end the drawing operation
if (m_CurrentDrawingOperation != null &&
( !allSplines.Contains(m_CurrentDrawingOperation.CurrentSplineInfo) ||
m_CurrentDrawingOperation.CurrentSplineInfo.Spline.Count == 0 ))
EndDrawingOperation();
DrawSplines(targets, allSplines, s_MainTarget);
if (m_CurrentDrawingOperation == null)
KnotPlacementHandle(allSplines, null, AddKnotOnKnot, AddKnotOnSurface, DrawKnotCreationPreview);
else
m_CurrentDrawingOperation.OnGUI(allSplines);
HandleCancellation();
}
// Curve id to SelectableKnotList - if we're inserting on a curve, we need 3 knots to preview the change, for other cases it's 2 knots
internal static List<(Spline spline, int curveIndex, List<BezierKnot> knots)> previewCurvesList = new();
void DrawSplines(IReadOnlyList<Object> targets, IReadOnlyList<SplineInfo> allSplines, Object mainTarget)
{
if (Event.current.type != EventType.Repaint)
return;
EditorSplineUtility.TryGetNearestKnot(allSplines, out SelectableKnot hoveredKnot);
foreach (var target in targets)
{
EditorSplineUtility.GetSplinesFromTarget(target, m_SplineBuffer);
bool isMainTarget = target == mainTarget;
var previewIndex = 0;
//Draw curves
foreach (var splineInfo in m_SplineBuffer)
{
var spline = splineInfo.Spline;
var localToWorld = splineInfo.LocalToWorld;
for (int i = 0, count = spline.GetCurveCount(); i < count; ++i)
{
if (previewIndex < previewCurvesList.Count)
{
var currentPreview = previewCurvesList[previewIndex];
if (currentPreview.spline.Equals(spline) && currentPreview.curveIndex == i)
{
var curveKnots = currentPreview.knots;
for (int knotIndex = 0; knotIndex + 1 < curveKnots.Count; ++knotIndex)
{
var previewCurve =
new BezierCurve(curveKnots[knotIndex], curveKnots[knotIndex + 1]);
previewCurve = previewCurve.Transform(localToWorld);
CurveHandles.Draw(previewCurve, isMainTarget);
}
previewIndex++;
continue;
}
}
var curve = spline.GetCurve(i).Transform(localToWorld);
CurveHandles.Draw(curve, isMainTarget);
if (isMainTarget)
CurveHandles.DrawFlow(curve, splineInfo.Spline, i);
}
}
//Draw knots
foreach (var splineInfo in m_SplineBuffer)
{
var spline = splineInfo.Spline;
for (int knotIndex = 0, count = spline.Count; knotIndex < count; ++knotIndex)
{
bool isHovered = hoveredKnot.SplineInfo.Equals(splineInfo) &&
hoveredKnot.KnotIndex == knotIndex;
if (isMainTarget || isHovered)
{
#if UNITY_2022_2_OR_NEWER
KnotHandles.Draw(new SelectableKnot(splineInfo, knotIndex), Handles.elementColor, false, isHovered);
#else
KnotHandles.Draw(new SelectableKnot(splineInfo, knotIndex), SplineHandleUtility.knotColor, false, isHovered);
#endif
if (SplineUtility.AreTangentsModifiable(spline.GetTangentMode(knotIndex)))
{
//Tangent In
if (spline.Closed || knotIndex != 0)
{
var tangentIn = new SelectableTangent(splineInfo, knotIndex, BezierTangent.In);
TangentHandles.DrawInformativeTangent(tangentIn, isMainTarget);
}
//Tangent Out
if (spline.Closed || knotIndex + 1 != spline.Count)
{
var tangentOut = new SelectableTangent(splineInfo, knotIndex, BezierTangent.Out);
TangentHandles.DrawInformativeTangent(tangentOut, isMainTarget);
}
}
}
else
KnotHandles.DrawInformativeKnot(new SelectableKnot(splineInfo, knotIndex));
}
}
}
}
void AddKnotOnKnot(SelectableKnot startFrom, float3 tangent)
{
Undo.RecordObject(startFrom.SplineInfo.Object, k_KnotPlacementUndoMessage);
EndDrawingOperation();
m_ActiveObjectIndex = GetTargetIndex(startFrom.SplineInfo);
// If we start from one of the ends of the spline we just append to that spline unless
// the spline is already closed or there is other links knots.
EditorSplineUtility.GetKnotLinks(startFrom, s_KnotsBuffer);
if (s_KnotsBuffer.Count == 1 && !startFrom.SplineInfo.Spline.Closed)
{
if (EditorSplineUtility.IsEndKnot(startFrom))
{
if (math.lengthsq(tangent) > float.Epsilon)
{
var current = startFrom;
var tOut = current.TangentOut;
current.Mode = TangentMode.Broken;
tOut.Direction = tangent;
}
m_CurrentDrawingOperation = new DrawingOperation(startFrom.SplineInfo,
DrawingOperation.DrawingDirection.End, false);
return;
}
if (startFrom.KnotIndex == 0)
{
if (math.lengthsq(tangent) > float.Epsilon)
{
var current = startFrom;
var tIn = current.TangentIn;
current.Mode = TangentMode.Broken;
tIn.Direction = tangent;
}
m_CurrentDrawingOperation = new DrawingOperation(startFrom.SplineInfo,
DrawingOperation.DrawingDirection.Start, false);
return;
}
}
// Otherwise we start a new spline
var knot = EditorSplineUtility.CreateSpline(startFrom, tangent);
EditorSplineUtility.LinkKnots(knot, startFrom);
m_CurrentDrawingOperation =
new DrawingOperation(knot.SplineInfo, DrawingOperation.DrawingDirection.End, true);
}
protected virtual void AddKnotOnSurface(float3 position, float3 normal, float3 tangentOut)
{
Undo.RecordObject(s_MainTarget, k_KnotPlacementUndoMessage);
EndDrawingOperation();
var container = (ISplineContainer)s_MainTarget;
// 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 ((container.Splines.Count == 1 && container.Splines[0].Count == 0
|| container.Splines.Count == 0 )
&& s_MainTarget.GetComponents<Component>().Length == 2)
{
if(!s_MainTarget.transform.hasChanged)
s_MainTarget.transform.position = position;
}
SplineInfo splineInfo;
// Spline gets created with an empty spline so we add to that spline first if needed
if (container.Splines.Count == 1 && container.Splines[0].Count == 0)
splineInfo = new SplineInfo(container, 0);
else
splineInfo = EditorSplineUtility.CreateSpline(container);
EditorSplineUtility.AddKnotToTheEnd(splineInfo, position, normal, tangentOut, false);
m_CurrentDrawingOperation = new DrawingOperation(splineInfo, DrawingOperation.DrawingDirection.End, false);
}
//SelectableKnot is not used and only here as this method is used as a `Action<float3, float3, float3, SelectableKnot>` by the `KnotPlacementHandle` method
void DrawKnotCreationPreview(float3 position, float3 normal, float3 tangentOut, SelectableKnot _)
{
if (!Mathf.Approximately(math.length(tangentOut), 0))
TangentHandles.Draw(position + tangentOut, position, normal);
if (s_MainTarget != null)
tangentOut = s_MainTarget.transform.InverseTransformVector(tangentOut);
#if UNITY_2022_2_OR_NEWER
KnotHandles.Draw(position, SplineUtility.GetKnotRotation(tangentOut, normal), Handles.elementColor, false, false);
#else
KnotHandles.Draw(position, SplineUtility.GetKnotRotation(tangentOut, normal), SplineHandleUtility.knotColor, false, false);
#endif
}
static void KnotPlacementHandle(
IReadOnlyList<SplineInfo> splines,
DrawingOperation drawingOperation,
Action<SelectableKnot, float3> createKnotOnKnot,
Action<float3, float3, float3> createKnotOnSurface,
Action<float3, float3, float3, SelectableKnot> drawPreview)
{
var controlId = GUIUtility.GetControlID(FocusType.Passive);
var evt = Event.current;
if (s_PlacementData != null
&& SceneView.currentDrawingSceneView != null
&& EditorWindow.mouseOverWindow == SceneView.currentDrawingSceneView
&& GUIUtility.hotControl != controlId)
s_PlacementData = null;
switch (evt.GetTypeForControl(controlId))
{
case EventType.Layout:
// Not using SplineHandles.ViewToolActive() here as otherwise it breaks the handles rendering when
// only pressing ALT and moving the mouse in the view.
if (!Tools.viewToolActive)
HandleUtility.AddDefaultControl(controlId);
break;
case EventType.Repaint:
{
var mousePosition = Event.current.mousePosition;
if (GUIUtility.hotControl == 0
&& SceneView.currentDrawingSceneView != null
&& EditorWindow.mouseOverWindow != SceneView.currentDrawingSceneView)
break;
if (GUIUtility.hotControl == 0)
{
if (EditorSplineUtility.TryGetNearestKnot(splines, out SelectableKnot knot))
{
drawPreview.Invoke(knot.Position, math.rotate(knot.Rotation, math.up()), float3.zero, knot);
}
else if (EditorSplineUtility.TryGetNearestPositionOnCurve(splines, out SplineCurveHit hit))
{
drawPreview.Invoke(hit.Position, hit.Normal, float3.zero, default);
}
else if (SplineHandleUtility.GetPointOnSurfaces(mousePosition, out Vector3 position,
out Vector3 normal))
{
drawPreview.Invoke(position, normal, float3.zero, default);
}
}
if (s_PlacementData != null)
{
var scale = s_MainTarget != null ? s_MainTarget.transform.lossyScale : Vector3.one;
var tan = new Vector3(s_PlacementData.TangentOut.x * scale.x,
s_PlacementData.TangentOut.y * scale.y,
s_PlacementData.TangentOut.z * scale.z);
drawPreview.Invoke(s_PlacementData.Position, s_PlacementData.Normal, tan,
default);
}
break;
}
case EventType.MouseMove:
var mouseMovePosition = Event.current.mousePosition;
previewCurvesList.Clear();
s_ClosestSpline = default;
var hasNearKnot = EditorSplineUtility.TryGetNearestKnot(splines, out SelectableKnot k);
if (hasNearKnot)
s_ClosestSpline = k.SplineInfo;
else if (EditorSplineUtility.TryGetNearestPositionOnCurve(splines, out SplineCurveHit curveHit))
{
s_ClosestSpline = curveHit.PreviousKnot.SplineInfo;
EditorSplineUtility.GetAffectedCurves(curveHit, previewCurvesList);
}
if (SplineHandleUtility.GetPointOnSurfaces(mouseMovePosition, out Vector3 pos, out Vector3 _))
{
if (drawingOperation != null)
{
var lastKnot = drawingOperation.GetLastAddedKnot();
var previousKnotIndex = drawingOperation.Direction == DrawingOperation.DrawingDirection.End
? drawingOperation.CurrentSplineInfo.Spline.PreviousIndex(lastKnot.KnotIndex)
: drawingOperation.CurrentSplineInfo.Spline.NextIndex(lastKnot.KnotIndex);
EditorSplineUtility.GetAffectedCurves(
drawingOperation.CurrentSplineInfo,
drawingOperation.CurrentSplineInfo.Transform.InverseTransformPoint(pos),
drawingOperation.Direction == DrawingOperation.DrawingDirection.Start,
lastKnot, previousKnotIndex, previewCurvesList);
}
}
if (HandleUtility.nearestControl == controlId)
HandleUtility.Repaint();
break;
case EventType.MouseDown:
{
if (evt.button != 0 || SplineHandles.ViewToolActive())
break;
if (HandleUtility.nearestControl == controlId)
{
GUIUtility.hotControl = controlId;
evt.Use();
var mousePosition = Event.current.mousePosition;
if (EditorSplineUtility.TryGetNearestKnot(splines, out SelectableKnot knot))
{
s_PlacementData = new KnotPlacementData(evt.mousePosition, knot);
}
else if (EditorSplineUtility.TryGetNearestPositionOnCurve(splines, out SplineCurveHit hit))
{
s_PlacementData = new CurvePlacementData(evt.mousePosition, hit);
}
else if (SplineHandleUtility.GetPointOnSurfaces(mousePosition, out Vector3 position,
out Vector3 normal))
{
s_PlacementData = new PlacementData(
evt.mousePosition,
position,
normal,
s_MainTarget != null ? s_MainTarget.transform.lossyScale : Vector3.one);
}
}
break;
}
case EventType.MouseDrag:
if (GUIUtility.hotControl == controlId && evt.button == 0)
{
evt.Use();
if (s_PlacementData != null
&& s_EnableDragTangent
&& Vector3.Distance(evt.mousePosition, s_PlacementData.MousePosition) > k_MinDragThreshold)
{
var ray = HandleUtility.GUIPointToWorldRay(evt.mousePosition);
if (s_PlacementData.Plane.Raycast(ray, out float distance))
{
var tangent = ( ray.origin + ray.direction * distance ) - s_PlacementData.Position;
s_PlacementData.TangentOut = new Vector3(
tangent.x / s_PlacementData.Scale.x,
tangent.y / s_PlacementData.Scale.y,
tangent.z / s_PlacementData.Scale.z );
}
}
}
break;
case EventType.MouseUp:
if (GUIUtility.hotControl == controlId && evt.button == 0)
{
GUIUtility.hotControl = 0;
evt.Use();
if (s_PlacementData != null)
{
var linkedKnot = s_PlacementData.GetOrCreateLinkedKnot();
if (linkedKnot.IsValid())
createKnotOnKnot.Invoke(linkedKnot, s_PlacementData.TangentOut);
else
createKnotOnSurface.Invoke(s_PlacementData.Position, s_PlacementData.Normal,
s_PlacementData.TangentOut);
s_PlacementData = null;
previewCurvesList.Clear();
}
}
break;
case EventType.KeyDown:
if (GUIUtility.hotControl == controlId &&
( evt.keyCode == KeyCode.Escape || evt.keyCode == KeyCode.Return ))
{
s_PlacementData = null;
GUIUtility.hotControl = 0;
evt.Use();
}
break;
}
}
void HandleCancellation()
{
var evt = Event.current;
if (GUIUtility.hotControl == 0 && evt.type == EventType.KeyDown)
{
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.Escape)
{
//If we are currently drawing, end the drawing operation and start a new one. If we haven't started drawing, switch to move tool instead
if (m_CurrentDrawingOperation != null)
{
ToolManager.SetActiveContext<SplineToolContext>();
ToolManager.SetActiveTool<SplineMoveTool>();
}
else
ToolManager.RestorePreviousTool();
evt.Use();
}
}
}
internal void EndDrawingOperation()
{
m_CurrentDrawingOperation?.Dispose();
m_CurrentDrawingOperation = null;
}
int GetTargetIndex(SplineInfo info)
{
return GetTargets().ToList().IndexOf(info.Object);
}
protected virtual IReadOnlyList<Object> GetSortedTargets(out Object mainTarget)
{
m_SortedTargets.Clear();
m_SortedTargets.AddRange(GetTargets());
if (m_ActiveObjectIndex >= m_SortedTargets.Count)
m_ActiveObjectIndex = 0;
mainTarget = m_SortedTargets[m_ActiveObjectIndex];
if (m_CurrentDrawingOperation != null)
mainTarget = m_CurrentDrawingOperation.CurrentSplineInfo.Object;
else if (!s_ClosestSpline.Equals(default))
mainTarget = s_ClosestSpline.Object;
// Move main target to the end for rendering/picking
m_SortedTargets.Remove(mainTarget);
m_SortedTargets.Add(mainTarget);
return m_SortedTargets;
}
static Vector3 ApplyIncrementalSnap(Vector3 current, Vector3 origin)
{
#if UNITY_2022_2_OR_NEWER
if (EditorSnapSettings.incrementalSnapActive)
return SplineHandleUtility.DoIncrementSnap(current, origin);
#endif
return current;
}
void CycleActiveTarget()
{
m_ActiveObjectIndex = ( m_ActiveObjectIndex + 1 ) % GetTargets().Count();
SceneView.RepaintAll();
}
protected virtual IEnumerable<Object> GetTargets()
{
return targets;
}
[Shortcut("Splines/Cycle Active Spline Container (Draw Spline Tool)", typeof(SceneView), KeyCode.S, ShortcutModifiers.Shift)]
static void ShortcutCycleActiveSplineContainer(ShortcutArguments args)
{
if (activeTool is KnotPlacementTool tool)
tool.CycleActiveTarget();
}
/// <summary>
/// Used for tests
/// </summary>
internal void AddKnotOnSurfaceInternal(Vector3 position, Vector3 tangentOut, bool endDrawing = false)
{
if (m_CurrentDrawingOperation == null)
AddKnotOnSurface(position, Vector3.up, tangentOut);
else
m_CurrentDrawingOperation.CreateKnotOnSurface(position, Vector3.up, tangentOut);
if(endDrawing)
EndDrawingOperation();
}
/// <summary>
/// Used for tests
/// </summary>
internal void AddKnotOnKnotInternal(int splineIndex, int knotIndex, Vector3 tangentOut, bool endDrawing = false)
{
var fromSplineInfo = new SplineInfo(MainTarget as SplineContainer, splineIndex);
if (m_CurrentDrawingOperation == null)
AddKnotOnKnot(new SelectableKnot(fromSplineInfo, knotIndex), tangentOut);
else
m_CurrentDrawingOperation.CreateKnotOnKnot(new SelectableKnot(fromSplineInfo, knotIndex), tangentOut);
if(endDrawing)
EndDrawingOperation();
}
}
}

View File

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

View File

@@ -0,0 +1,74 @@
using UnityEditor.EditorTools;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
[CustomEditor(typeof(SplineMoveTool))]
class SplineMoveToolSettings : SplineToolSettings { }
/// <summary>
/// Provides methods that move knots and tangents in the Scene view. This tool is only available when you use SplineToolContext.
/// `SplineMoveTool` works similarly to the Move tool for GameObjects, except that it has extra handle configurations according to the `handleOrientation` settings.
/// `SplineToolContext` manages the selection of knots and tangents. You can manipulate the selection of knots and tangents with `SplineMoveTool`.
/// </summary>
#if UNITY_2021_2_OR_NEWER
[EditorTool("Spline Move Tool", typeof(ISplineContainer), typeof(SplineToolContext))]
#else
[EditorTool("Spline Move Tool", typeof(ISplineContainer))]
#endif
public sealed class SplineMoveTool : SplineTool
{
/// <inheritdoc />
public override bool gridSnapEnabled
{
get => handleOrientation == HandleOrientation.Global;
}
/// <inheritdoc />
public override GUIContent toolbarIcon => PathIcons.splineMoveTool;
/// <inheritdoc />
public override void OnToolGUI(EditorWindow window)
{
switch (Event.current.type)
{
case EventType.Layout:
UpdatePivotPosition();
break;
case 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;
break;
case EventType.MouseUp:
TransformOperation.pivotFreeze = TransformOperation.PivotFreeze.None;
UpdatePivotPosition();
UpdateHandleRotation();
break;
}
if (TransformOperation.canManipulate && !DirectManipulation.IsDragging)
{
EditorGUI.BeginChangeCheck();
var newPos = Handles.DoPositionHandle(pivotPosition, handleRotation);
if (EditorGUI.EndChangeCheck())
{
EditorSplineUtility.RecordSelection($"Move Spline Elements ({SplineSelection.Count})");
TransformOperation.ApplyTranslation(newPos - pivotPosition);
if (Tools.pivotMode == PivotMode.Center)
TransformOperation.ForcePivotPosition(newPos);
}
}
}
}
}

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,68 @@
using UnityEngine;
using UnityEditor.EditorTools;
using UnityEditor.Overlays;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
[CustomEditor(typeof(SplineRotateTool))]
class SplineRotateToolSettings : SplineToolSettings { }
/// <summary>
/// Provides methods to rotate knots and tangents in the Scene view. This tool is only available when you use SplineToolContext.
/// `SplineRotateTool` is similar to the Rotate tool for GameObjects except that it has extra handle configurations according to the `handleOrientation` settings.
/// The rotation of tangents are usually related to the rotation of their knots, except when tangents use the Broken Bezier tangent mode. The rotation of tangents that use the Broken Bezier tangent mode are independent from the rotation of their knot.
/// `SplineToolContext` manages the selection of knots and tangents. You can manipulate the selection of knots and tangents with `SplineRotateTool`.
/// </summary>
#if UNITY_2021_2_OR_NEWER
[EditorTool("Spline Rotate", typeof(ISplineContainer), typeof(SplineToolContext))]
#else
[EditorTool("Spline Rotate", typeof(ISplineContainer))]
#endif
public sealed class SplineRotateTool : SplineTool
{
/// <inheritdoc />
public override GUIContent toolbarIcon => PathIcons.splineRotateTool;
Quaternion m_CurrentRotation = Quaternion.identity;
Vector3 m_RotationCenter = Vector3.zero;
/// <inheritdoc />
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;
UpdateHandleRotation();
}
if (Event.current.type == EventType.Layout)
UpdatePivotPosition(true);
if(TransformOperation.canManipulate && !DirectManipulation.IsDragging)
{
EditorGUI.BeginChangeCheck();
var rotation = Handles.DoRotationHandle(m_CurrentRotation, m_RotationCenter);
if(EditorGUI.EndChangeCheck())
{
EditorSplineUtility.RecordSelection($"Rotate Spline Elements ({SplineSelection.Count})");
TransformOperation.ApplyRotation(rotation * Quaternion.Inverse(m_CurrentRotation), m_RotationCenter);
m_CurrentRotation = rotation;
}
if(GUIUtility.hotControl == 0)
{
UpdateHandleRotation();
m_CurrentRotation = handleRotation;
m_RotationCenter = pivotPosition;
}
}
}
}
}

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,65 @@
using UnityEditor.EditorTools;
using UnityEditor.Overlays;
using UnityEngine;
using UnityEngine.Splines;
namespace UnityEditor.Splines
{
[CustomEditor(typeof(SplineScaleTool))]
class SplineScaleToolSettings : SplineToolSettings { }
/// <summary>
/// Provides methods to scale knots and tangents in the Scene view. This tool is only available when you use SplineToolContext.
/// When you scale a knot, you also scale both its tangents and change the curvature of the segment around the knot.
/// `SplineToolContext` manages the selection of knots and tangents. You can manipulate the selection of knots and tangents with `SplineRotateTool`.
/// </summary>
#if UNITY_2021_2_OR_NEWER
[EditorTool("Spline Scale Tool", typeof(ISplineContainer), typeof(SplineToolContext))]
#else
[EditorTool("Spline Scale Tool", typeof(ISplineContainer))]
#endif
public sealed class SplineScaleTool : SplineTool
{
/// <inheritdoc />
public override GUIContent toolbarIcon => PathIcons.splineScaleTool;
Vector3 m_currentScale = Vector3.one;
/// <inheritdoc />
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)
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;
UpdateHandleRotation();
}
if(TransformOperation.canManipulate && !DirectManipulation.IsDragging)
{
EditorGUI.BeginChangeCheck();
m_currentScale = Handles.DoScaleHandle(m_currentScale, pivotPosition, handleRotation, HandleUtility.GetHandleSize(pivotPosition));
if (EditorGUI.EndChangeCheck())
{
EditorSplineUtility.RecordSelection($"Scale Spline Elements ({SplineSelection.Count})");
TransformOperation.ApplyScale(m_currentScale);
}
}
}
}
}

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,279 @@
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. 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.
/// </summary>
public 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
public abstract class SplineToolSettings : UnityEditor.Editor, ICreateToolbar
{
public IEnumerable<string> toolbarElements
{
#else
abstract class SplineToolSettings : CreateToolbarBase
{
protected override IEnumerable<string> 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
}
}
}
/// <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>
public abstract class SplineTool : EditorTool
{
/// <summary>The current orientation of the handles for the tool in use.</summary>
static UserSetting<HandleOrientation> m_HandleOrientation = new UserSetting<HandleOrientation>(PathSettings.instance, "SplineTool.HandleOrientation", HandleOrientation.Global, SettingsScope.User);
/// <summary>The current orientation of the handles for the current spline tool.</summary>
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;
/// <summary>
/// The current active SplineTool in use.
/// </summary>
// Workaround for lack of access to ShortcutContext. Use this to pass shortcut actions to tool instances.
protected static SplineTool activeTool { get; private set; }
/// <summary>
/// The current position of the pivot regarding the selection.
/// </summary>
public static Vector3 pivotPosition => TransformOperation.pivotPosition;
/// <summary>
/// The current rotation of the handle regarding the selection and the Handle Rotation configuration.
/// </summary>
public static Quaternion handleRotation => TransformOperation.handleRotation;
/// <summary>
/// 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.
/// </summary>
public static void UpdateHandleRotation() => TransformOperation.UpdateHandleRotation();
/// <summary>
/// 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.
/// </summary>
/// <param name="useKnotPositionForTangents">
/// 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.
/// </param>
public static void UpdatePivotPosition(bool useKnotPositionForTangents = false) => TransformOperation.UpdatePivotPosition(useKnotPositionForTangents);
/// <summary>
/// Invoked after this EditorTool becomes the active tool.
/// </summary>
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;
}
/// <summary>
/// Invoked before this EditorTool stops being the active tool.
/// </summary>
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;
}
/// <summary>
/// Callback invoked when the handle rotation configuration changes.
/// </summary>
protected virtual void OnHandleOrientationChanged()
{
UpdateHandleRotation();
}
static void OnPivotRotationChanged()
{
handleOrientation = (HandleOrientation)Tools.pivotRotation;
}
/// <summary>
/// Callback invoked when the pivot mode configuration changes.
/// </summary>
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;
}
}
}
}

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,467 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.Mathematics;
using UnityEditor.EditorTools;
using UnityEngine;
using UnityEngine.Splines;
using UObject = UnityEngine.Object;
#if UNITY_2023_2_OR_NEWER
using UnityEngine.UIElements;
using UnityEditor.Actions;
#endif
#if UNITY_2022_1_OR_NEWER
using UnityEditor.Overlays;
#endif
namespace UnityEditor.Splines
{
#if UNITY_2022_1_OR_NEWER
[CustomEditor(typeof(SplineToolContext))]
class SplineToolContextSettings : UnityEditor.Editor, ICreateToolbar
{
public IEnumerable<string> toolbarElements
{
get
{
yield return "Spline Tool Settings/Handle Visuals";
}
}
}
#endif
/// <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(ISplineContainer)), Icon(k_IconPath)]
#else
[EditorToolContext("Spline", typeof(ISplineContainer))]
#endif
public sealed class SplineToolContext : EditorToolContext
{
const string k_IconPath = "Packages/com.unity.splines/Editor/Resources/Icons/SplineContext.png";
static bool s_UseCustomSplineHandles = false;
readonly SplineElementRectSelector m_RectSelector = new SplineElementRectSelector();
readonly List<SplineInfo> m_Splines = new List<SplineInfo>();
readonly List<SelectableTangent> m_TangentBuffer = new List<SelectableTangent>();
bool m_WasActiveAfterDeserialize;
static SplineToolContext s_Instance;
/// <summary>
/// Defines if the spline tool context draws the default handles for splines or if they are managed directly by the `SplineTool`.
/// Set to false for SplineToolContext to draw the default spline handles for segments, knots, and tangents which you can directly manipulate. Set to true to draw handles and manage direct manipulation with your tool. The default value is false.
/// </summary>
public static bool useCustomSplineHandles
{
set => s_UseCustomSplineHandles = value;
}
/// <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;
}
#if UNITY_2023_2_OR_NEWER
/// <summary>
/// Populate the scene view context menu with Spline actions
/// </summary>
/// <param name="menu"></param>
public override void PopulateMenu(DropdownMenu menu)
{
List<SelectableKnot> knotBufferOnContextMenuOpen = new List<SelectableKnot>();
SplineSelection.GetElements<SelectableKnot>(m_Splines, knotBufferOnContextMenuOpen);
bool cutEnabled = false;
bool copyEnabled = SplineSelection.HasAny<SelectableKnot>(m_Splines);
bool pasteEnabled = CopyPaste.IsSplineCopyBuffer(GUIUtility.systemCopyBuffer);
bool duplicateEnabled = copyEnabled;
bool deleteEnabled = true;
ContextMenuUtility.AddClipboardEntriesTo(menu, cutEnabled, copyEnabled, pasteEnabled, duplicateEnabled, deleteEnabled);
menu.AppendSeparator();
menu.AppendAction("Link Knots",
action => {
EditorSplineUtility.LinkKnots(knotBufferOnContextMenuOpen);
},
action => SplineSelectionUtility.CanLinkKnots(knotBufferOnContextMenuOpen)
? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled );
menu.AppendAction("Unlink Knots",
action => {
EditorSplineUtility.UnlinkKnots(knotBufferOnContextMenuOpen);
},
action => SplineSelectionUtility.CanUnlinkKnots(knotBufferOnContextMenuOpen)
? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
menu.AppendAction("Split Knot",
action => {
EditorSplineUtility.RecordSelection("Split knot");
SplineSelection.Set(EditorSplineUtility.SplitKnot(knotBufferOnContextMenuOpen[0]));
},
action => SplineSelectionUtility.CanSplitSelection(knotBufferOnContextMenuOpen)
? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
menu.AppendAction("Join Knots",
action => {
EditorSplineUtility.RecordSelection("Join knot");
SplineSelection.Set(EditorSplineUtility.JoinKnots(knotBufferOnContextMenuOpen[0], knotBufferOnContextMenuOpen[1]));
},
action => SplineSelectionUtility.CanJoinSelection(knotBufferOnContextMenuOpen)
? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
menu.AppendAction("Reverse Spline Flow",
action => {
EditorSplineUtility.RecordSelection("Reverse Selected Splines Flow");
EditorSplineUtility.ReverseSplinesFlow(m_Splines);
},
action => SplineSelection.Count > 0 ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled);
}
#endif
/// <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">The window that is displaying this active context.</param>
public override void OnToolGUI(EditorWindow window)
{
UpdateSelectionIfSplineRemoved(m_Splines);
EditorSplineUtility.GetSplinesFromTargets(targets, m_Splines);
//TODO set active spline
if (Event.current.type == EventType.Layout)
SplineInspectorOverlay.SetSelectedSplines(m_Splines);
m_RectSelector.OnGUI(m_Splines);
if (!s_UseCustomSplineHandles)
{
foreach (var sInfo in m_Splines)
{
if (sInfo.Transform.hasChanged)
{
SplineCacheUtility.ClearCache();
sInfo.Transform.hasChanged = false;
}
}
SplineHandles.DoHandles(m_Splines);
}
SplineHandleUtility.canDrawOnCurves = false;
HandleCommands();
}
void OnEnable()
{
AssemblyReloadEvents.afterAssemblyReload += OnAfterDomainReload;
ToolManager.activeContextChanged += ContextChanged;
UpdateSelection();
}
/// <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;
Spline.afterSplineWasModified += OnSplineWasModified;
Undo.undoRedoPerformed += UndoRedoPerformed;
SplineCacheUtility.InitializeCache();
s_Instance = this;
}
/// <summary>
/// Invoked before this EditorToolContext stops being the active tool context.
/// </summary>
public override void OnWillBeDeactivated()
{
Spline.afterSplineWasModified -= OnSplineWasModified;
Undo.undoRedoPerformed -= UndoRedoPerformed;
SplineCacheUtility.ClearCache();
s_Instance = null;
}
void UpdateSelectionIfSplineRemoved(List<SplineInfo> previousSelection)
{
foreach (var splineInfo in previousSelection)
{
if (!EditorSplineUtility.Exists(splineInfo))
{
UpdateSelection();
return;
}
}
}
void ContextChanged()
{
if (!ToolManager.IsActiveContext(this))
SplineSelection.ClearNoUndo(false);
}
void OnSplineWasModified(Spline spline)
{
//Only updating selection is spline is in the selected m_Splines
if(m_Splines.Count(s => s.Spline == spline) > 0)
UpdateSelection();
}
void UndoRedoPerformed() => UpdateSelection();
void UpdateSelection()
{
SplineSelection.UpdateObjectSelection(targets);
SceneView.RepaintAll();
}
internal static IEnumerable<UObject> GetTargets()
{
if (s_Instance)
return s_Instance.targets;
return null;
}
void DeleteSelected()
{
var selectedElements = SplineSelection.Count;
if (selectedElements > 0)
{
EditorSplineUtility.RecordSelection($"Delete selected elements ({selectedElements})");
List<SplineInfo> splinesToRemove = new List<SplineInfo>();
// First delete the knots in selection
var knotBuffer = new List<SelectableKnot>();
SplineSelection.GetElements(m_Splines, knotBuffer);
if (knotBuffer.Count > 0)
{
//Sort knots index so removing them doesn't cause the rest of the indices to be invalid
knotBuffer.Sort((a, b) => a.KnotIndex.CompareTo(b.KnotIndex));
for (int i = knotBuffer.Count - 1; i >= 0; --i)
{
EditorSplineUtility.RemoveKnot(knotBuffer[i]);
var spline = knotBuffer[i].SplineInfo;
if (EditorSplineUtility.ShouldRemoveSpline(spline) && !splinesToRemove.Contains(spline))
splinesToRemove.Add(spline);
}
}
// "Delete" remaining tangents by zeroing them out
SplineSelection.GetElements(m_Splines, m_TangentBuffer);
for (int i = m_TangentBuffer.Count - 1; i >= 0; --i)
EditorSplineUtility.ClearTangent(m_TangentBuffer[i]);
// Sort spline index so removing them doesn't cause the rest of the indices to be invalid
splinesToRemove.Sort((a, b) => a.Index.CompareTo(b.Index));
for (int i = splinesToRemove.Count - 1; i >= 0; --i)
{
var spline = splinesToRemove[i];
SplineSelection.Remove(spline);
spline.Container.RemoveSplineAt(spline.Index);
if (spline.Object != null)
PrefabUtility.RecordPrefabInstancePropertyModifications(spline.Object);
}
}
}
/// <summary>
/// This methods automatically frames the selected splines or spline elements in the scene view.
/// </summary>
public void FrameSelected()
{
Bounds selectionBounds;
if (TransformOperation.canManipulate)
{
selectionBounds = TransformOperation.GetSelectionBounds(false);
selectionBounds.Encapsulate(TransformOperation.pivotPosition);
}
else
selectionBounds = EditorSplineUtility.GetBounds(m_Splines);
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);
}
void HandleCommands()
{
Event evt = Event.current;
var cmd = evt.commandName;
if (evt.type == EventType.ValidateCommand)
{
switch (cmd)
{
case "SelectAll":
case "Delete":
case "SoftDelete":
case "FrameSelected":
evt.Use();
break;
case "Duplicate":
case "Copy":
if (SplineSelection.HasAny<SelectableKnot>(m_Splines))
evt.Use();
break;
case "Paste":
if (CopyPaste.IsSplineCopyBuffer(GUIUtility.systemCopyBuffer))
evt.Use();
break;
}
}
else if (evt.type == EventType.ExecuteCommand)
{
switch (cmd)
{
case "SelectAll":
{
SelectAll();
evt.Use();
break;
}
case "Copy":
{
var knotBuffer = new List<SelectableKnot>();
SplineSelection.GetElements(m_Splines, knotBuffer);
GUIUtility.systemCopyBuffer = CopyPaste.Copy(knotBuffer);
evt.Use();
break;
}
case "Paste":
{
CopyPaste.Paste(GUIUtility.systemCopyBuffer);
evt.Use();
break;
}
case "Duplicate":
{
var knotBuffer = new List<SelectableKnot>();
var splineBuffer = new List<SplineInfo>();
foreach (var t in targets)
{
if (t is ISplineContainer container)
{
EditorSplineUtility.GetSplinesFromTarget(t, splineBuffer);
SplineSelection.GetElements(m_Splines, knotBuffer);
string copyPasteBuffer = CopyPaste.Copy(knotBuffer);
CopyPaste.Paste(copyPasteBuffer, container);
}
}
evt.Use();
break;
}
case "Delete":
case "SoftDelete":
{
DeleteSelected();
evt.Use();
break;
}
case "FrameSelected":
{
FrameSelected();
evt.Use();
break;
}
}
}
}
void SelectAll()
{
var knots = new List<SelectableKnot>();
var tangents = new List<SelectableTangent>(knots.Count() * 2);
foreach (var info in m_Splines)
{
if (!SplineSelection.HasActiveSplineSelection() || SplineSelection.Contains(info))
{
for (int knotIdx = 0; knotIdx < info.Spline.Count; ++knotIdx)
{
knots.Add(new SelectableKnot(info, knotIdx));
void TryAddSelectableTangent(BezierTangent tan)
{
var t = new SelectableTangent(info, knotIdx, tan);
if (SplineSelectionUtility.IsSelectable(t))
tangents.Add(t);
}
TryAddSelectableTangent(BezierTangent.In);
TryAddSelectableTangent(BezierTangent.Out);
}
}
}
SplineSelection.AddRange(knots);
SplineSelection.AddRange(tangents);
}
void OnAfterDomainReload()
{
m_WasActiveAfterDeserialize = ToolManager.activeContextType == typeof(SplineToolContext);
AssemblyReloadEvents.afterAssemblyReload -= OnAfterDomainReload;
}
internal static Spline GetSpline(UObject target, int targetIndex)
{
if (target is ISplineContainer provider)
return provider.Splines.ElementAt(targetIndex);
return null;
}
}
}

View File

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

View File

@@ -0,0 +1,551 @@
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(ISelectableElement element)
{
var tData = new TransformData();
tData.position = new float3(element.Position);
var knot = new SelectableKnot(element.SplineInfo, element.KnotIndex);
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 readonly List<ISelectableElement> s_ElementSelection = new List<ISelectableElement>(32);
public static IReadOnlyList<ISelectableElement> elementSelection => s_ElementSelection;
static int s_ElementSelectionCount = 0;
public static bool canManipulate => s_ElementSelectionCount > 0;
static ISelectableElement 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<SelectableKnot> s_RotatedKnotCache = new HashSet<SelectableKnot>();
// Used to prevent the translation of the same knot multiple times if a linked knot was moved
static HashSet<SelectableKnot> s_LinkedKnotCache = new HashSet<SelectableKnot>();
static readonly List<SelectableKnot> s_KnotBuffer = new List<SelectableKnot>();
static RotationSyncData s_RotationSyncData = new RotationSyncData();
internal static void UpdateSelection(IEnumerable<Object> selection)
{
SplineSelection.GetElements(EditorSplineUtility.GetSplinesFromTargetsInternal(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 = EditorSplineUtility.GetElementBounds(s_ElementSelection, useKnotPositionForTangents).center;
break;
case PivotMode.Pivot:
if (s_ElementSelectionCount == 0)
goto default;
var element = s_ElementSelection[0];
if (useKnotPositionForTangents && element is SelectableTangent 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 = EditorSplineUtility.GetElementRotation(curElement);
else if (curElement is SelectableTangent editableTangent)
handleRotation = EditorSplineUtility.GetElementRotation(editableTangent.Owner);
}
s_HandleRotation = handleRotation;
s_HandleRotationInv = math.inverse(s_HandleRotation);
}
public static void ApplyTranslation(float3 delta)
{
s_RotatedKnotCache.Clear();
s_LinkedKnotCache.Clear();
foreach (var element in s_ElementSelection)
{
if (element is SelectableKnot knot)
{
if (!s_LinkedKnotCache.Contains(knot))
{
knot.Position = ApplySmartRounding(knot.Position + delta);
EditorSplineUtility.GetKnotLinks(knot, s_KnotBuffer);
foreach (var k in s_KnotBuffer)
s_LinkedKnotCache.Add(k);
if (!s_RotationSyncData.initialized)
s_RotationSyncData.Initialize(quaternion.identity, 0f, 1f);
}
}
else if (element is SelectableTangent tangent)
{
knot = tangent.Owner;
//Do nothing on the tangent if the knot is also in the selection
if (s_ElementSelection.Contains(knot))
continue;
if (OppositeTangentSelected(tangent))
knot.Mode = TangentMode.Broken;
if (knot.Mode == TangentMode.Broken)
tangent.Position = ApplySmartRounding(knot.Position + tangent.Direction + delta);
else
{
if (s_RotatedKnotCache.Contains(knot))
continue;
// Build rotation sync data based on active selection's transformation
if (!s_RotationSyncData.initialized)
{
var newTangentPosWorld = knot.Position + tangent.Direction + delta;
var deltas = CalculateMirroredTangentTranslationDeltas(tangent, newTangentPosWorld);
s_RotationSyncData.Initialize(deltas.knotRotationDelta, deltas.tangentLocalMagnitudeDelta, 1f);
}
ApplyTangentRotationSyncTransform(tangent);
}
}
}
s_RotationSyncData.Clear();
}
public static void ApplyRotation(Quaternion deltaRotation, float3 rotationCenter)
{
s_RotatedKnotCache.Clear();
foreach (var element in s_ElementSelection)
{
if (element is SelectableKnot 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 SelectableTangent tangent && !s_ElementSelection.Contains(tangent.Owner))
{
knot = tangent.Owner;
if (knot.Mode == TangentMode.Broken)
{
if (Tools.pivotMode == PivotMode.Pivot)
rotationCenter = knot.Position;
var mode = knot.Mode;
var deltaPos = math.rotate(deltaRotation, tangent.Position - rotationCenter);
tangent.Position = deltaPos + rotationCenter;
}
else
{
if (s_RotatedKnotCache.Contains(tangent.Owner))
continue;
deltaRotation.ToAngleAxis(out var deltaRotationAngle, out var deltaRotationAxis);
if (math.abs(deltaRotationAngle) > 0f)
{
if (knot.Mode != TangentMode.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 - knot.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))
knot.Mode = TangentMode.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 - rotationCenter);
var knotToRotationCenter = 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(SelectableTangent tangent)
{
if (tangent.Owner.Mode != TangentMode.Broken)
if (s_ElementSelection.Contains(tangent.OppositeTangent))
return true;
return false;
}
static void RotateKnot(SelectableKnot knot, quaternion deltaRotation, float3 rotationCenter, bool allowTranslation = true)
{
var knotInBrokenMode = knot.Mode == TangentMode.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();
ISelectableElement[] scaledElements = new ISelectableElement[s_ElementSelectionCount];
for (int elementIndex = 0; elementIndex < s_ElementSelectionCount; elementIndex++)
{
var element = s_ElementSelection[elementIndex];
if (element is SelectableKnot knot)
{
ScaleKnot(knot, elementIndex, scale);
if (!s_RotationSyncData.initialized)
s_RotationSyncData.Initialize(quaternion.identity, 0f, 1f);
}
else if (element is SelectableTangent tangent && !s_ElementSelection.Contains(tangent.Owner))
{
var owner = tangent.Owner;
var restoreMode = false;
var mode = owner.Mode;
var scaleDelta = scale - new float3(1f, 1f, 1f);
if (mode != TangentMode.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 - owner.Position;
// Enter broken mode if scale operation center does not match owner knot's position
if (!Mathf.Approximately(math.length(knotToCenter), 0f))
{
owner.Mode = TangentMode.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 (owner.Mode == TangentMode.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 newTangentPosWorld = ScaleTangent(tangent, s_MouseDownData[elementIndex].position, scale);
var deltas = CalculateMirroredTangentTranslationDeltas(tangent, newTangentPosWorld);
var scaleMultiplier = 1f + deltas.tangentLocalMagnitudeDelta / math.length(tangent.LocalDirection);
s_RotationSyncData.Initialize(deltas.knotRotationDelta, 0f, scaleMultiplier);
}
if (owner.Mode == TangentMode.Mirrored && s_RotatedKnotCache.Contains(owner))
continue;
ApplyTangentRotationSyncTransform(tangent, false);
}
if (restoreMode)
owner.Mode = mode;
}
}
scaledElements[elementIndex] = element;
}
s_RotationSyncData.Clear();
}
static void ScaleKnot(SelectableKnot 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;
}
var tangent = knot.TangentIn;
tangent.Direction = math.rotate(s_HandleRotation, math.rotate(s_HandleRotationInv, s_MouseDownData[dataIndex].inTangentDirection) * scale);
tangent = knot.TangentOut;
tangent.Direction = math.rotate(s_HandleRotation, math.rotate(s_HandleRotationInv, s_MouseDownData[dataIndex].outTangentDirection) * scale);
}
static float3 ScaleTangent(SelectableTangent 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(SelectableTangent tangent, bool absoluteScale = true)
{
if (tangent.Equals(currentElementSelected) ||
tangent.Owner.Mode == TangentMode.Mirrored ||
(!absoluteScale && tangent.Owner.Mode == TangentMode.Continuous))
{
if (absoluteScale)
{
var localDirection = tangent.LocalDirection;
if (Mathf.Approximately(math.length(localDirection), 0f))
localDirection = new float3(0, 0, 1f);
tangent.LocalDirection += math.normalizesafe(localDirection) * s_RotationSyncData.magnitudeDelta;
}
else
tangent.LocalDirection *= s_RotationSyncData.scaleMultiplier;
}
RotateKnot(tangent.Owner, s_RotationSyncData.rotationDelta, tangent.Owner.Position, false);
}
/*
Given a mirrored tangent and a target position, calculate the knot rotation delta and tangent's local magnitude change required to
put the tangent into target world position while fully respecting the owner spline's transformation (including non-uniform scale).
*/
internal static (quaternion knotRotationDelta, float tangentLocalMagnitudeDelta) CalculateMirroredTangentTranslationDeltas(SelectableTangent tangent, float3 targetPosition)
{
var knot = tangent.Owner;
var splineTrsInv = math.inverse(knot.SplineInfo.LocalToWorld);
var splineTrs = knot.SplineInfo.LocalToWorld;
var splinePos = splineTrs.c3.xyz;
var splineRotation = new quaternion(splineTrs);
var unscaledTargetPos = splinePos + math.rotate(splineRotation, math.transform(splineTrsInv, targetPosition));
var unscaledCurrentPos = splinePos + math.rotate(splineRotation, math.transform(splineTrsInv, tangent.Position));
var unscaledKnotPos = splinePos + math.rotate(splineRotation, math.transform(splineTrsInv, knot.Position));
var knotRotationInv = math.inverse(knot.Rotation);
var forward = (tangent.TangentIndex == 0 ? -1f : 1f) * math.normalizesafe(unscaledTargetPos - unscaledKnotPos);
var up = math.mul(knot.Rotation, math.up());
var knotLookAtRotation = quaternion.LookRotationSafe(forward, up);
var knotRotationDelta = math.mul(knotLookAtRotation, knotRotationInv);
var targetLocalDirection = math.rotate(knotRotationInv, (unscaledTargetPos - unscaledKnotPos));
var tangentLocalMagnitudeDelta = math.length(targetLocalDirection) - math.length(tangent.LocalDirection);
return (knotRotationDelta, tangentLocalMagnitudeDelta);
}
static SelectableKnot GetCurrentSelectionKnot()
{
if (currentElementSelected == null)
return default;
if (currentElementSelected is SelectableTangent tangent)
return tangent.Owner;
if (currentElementSelected is SelectableKnot knot)
return knot;
return default;
}
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 EditorSplineUtility.GetElementBounds(s_ElementSelection, useKnotPositionForTangents);
}
public static float3 ApplySmartRounding(float3 position)
{
//If we are snapping, disable the smart rounding. If not the case, the transform will have the wrong snap value based on distance to screen.
#if UNITY_2022_2_OR_NEWER
if (EditorSnapSettings.incrementalSnapActive || EditorSnapSettings.gridSnapActive)
return position;
#endif
float3 minDifference = SplineHandleUtility.GetMinDifference(position);
for (int i = 0; i < 3; ++i)
position[i] = Mathf.Approximately(position[i], 0f) ? position[i] : SplineHandleUtility.RoundBasedOnMinimumDifference(position[i], minDifference[i]);
return position;
}
}
}

View File

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