364 lines
15 KiB
C#
364 lines
15 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|