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 m_Splines = new List(); 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(); } } 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().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 results) { results.Clear(); foreach (var t in targets) { IReadOnlyList 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(); } } }