using System; using System.Linq; using Unity.Mathematics; using UnityEngine; using UnityEngine.Splines; namespace UnityEditor.Splines { /// /// Provides methods for drawing manipulation handles. /// public static class SplineDataHandles { const float k_HandleSize = 0.15f; const int k_PickRes = 2; static int[] s_DataPointsIDs; static int s_NewDataPointIndex = -1; static bool s_AddingDataPoint = false; static bool m_ShowAddHandle; static float3 m_Position; static float m_T; /// /// Draw default manipulation handles that enables adding, removing and moving /// DataPoints of the targeted SplineData along a Spline. Left click on an empty location /// on the spline adds a new DataPoint in the SplineData. Left click on an existing DataPoint /// allows to move this point along the Spline while a right click on it allows to delete that DataPoint. /// /// The Spline to use to interpret the SplineData. /// The SplineData for which the handles are drawn. /// The Spline type. /// The type of data this data point stores. public static void DataPointHandles( this TSpline spline, SplineData splineData) where TSpline : ISpline { var evt = Event.current; if(evt.type == EventType.MouseMove) { //Compute distance to spline and closest point var ray = HandleUtility.GUIPointToWorldRay(evt.mousePosition); var distance = SplineUtility.GetNearestPoint(spline, ray, out m_Position, out m_T); m_ShowAddHandle = distance < HandleUtility.GetHandleSize(m_Position); } //Id has to be consistent no matter the distance test var id = GUIUtility.GetControlID(FocusType.Passive); //Only activating the tooling when close enough from the spline if(m_ShowAddHandle) DataPointAddHandle(id, spline, splineData, m_Position, m_T); //Remove DataPoint functionality TryRemoveDataPoint(splineData); //Draw Default manipulation handles DataPointMoveHandles(spline, splineData); } static void TryRemoveDataPoint(SplineData splineData) { var evt = Event.current; //Remove data point only when not adding one and when using right click button if(!s_AddingDataPoint && GUIUtility.hotControl == 0 && evt.type == EventType.MouseDown && evt.button == 1 && s_DataPointsIDs.Contains(HandleUtility.nearestControl)) { var dataPointIndex = splineData.Indexes.ElementAt(Array.IndexOf(s_DataPointsIDs, HandleUtility.nearestControl)); splineData.RemoveDataPoint(dataPointIndex); evt.Use(); } } static void DataPointAddHandle( int controlID, TSpline spline, SplineData splineData, float3 pos, float t) where TSpline : ISpline { Event evt = Event.current; EventType eventType = evt.GetTypeForControl(controlID); switch (eventType) { case EventType.Layout: { if(!Tools.viewToolActive) HandleUtility.AddControl(controlID, HandleUtility.DistanceToCircle(pos, 0.1f)); break; } case EventType.Repaint: if(HandleUtility.nearestControl == controlID && GUIUtility.hotControl == 0 || s_AddingDataPoint) { var upDir = spline.EvaluateUpVector(t); Handles.CircleHandleCap(controlID, pos, Quaternion.LookRotation(upDir), 0.15f * HandleUtility.GetHandleSize(pos), EventType.Repaint); } break; case EventType.MouseDown: if (evt.button == 0 && HandleUtility.nearestControl == controlID && GUIUtility.hotControl == 0) { s_AddingDataPoint = true; var index = SplineUtility.ConvertIndexUnit( spline, t, splineData.PathIndexUnit); s_NewDataPointIndex = splineData.AddDataPointWithDefaultValue(index); evt.Use(); } break; case EventType.MouseDrag: if (evt.button == 0 && s_AddingDataPoint) { GUIUtility.hotControl = 0; var index = SplineUtility.ConvertIndexUnit( spline, t, splineData.PathIndexUnit); s_NewDataPointIndex = splineData.MoveDataPoint(s_NewDataPointIndex, index); evt.Use(); } break; case EventType.MouseUp: if (evt.button == 0 && s_AddingDataPoint) { s_AddingDataPoint = false; s_NewDataPointIndex = -1; GUIUtility.hotControl = 0; evt.Use(); } break; case EventType.MouseMove: HandleUtility.Repaint(); break; } } static void DataPointMoveHandles(TSpline spline, SplineData splineData) where TSpline : ISpline { if(s_DataPointsIDs == null || s_DataPointsIDs.Length != splineData.Count) s_DataPointsIDs = new int[splineData.Count]; //Cache all data point IDs for(int dataIndex = 0; dataIndex < splineData.Count; dataIndex++) s_DataPointsIDs[dataIndex] = GUIUtility.GetControlID(FocusType.Passive); //Draw all data points handles on the spline for(int dataIndex = 0; dataIndex < splineData.Count; dataIndex++) { var id = GUIUtility.GetControlID(FocusType.Passive); var index = splineData.Indexes.ElementAt(dataIndex); SplineDataHandle( s_DataPointsIDs[dataIndex], spline, splineData, index, k_HandleSize, out float newIndex); if(GUIUtility.hotControl == s_DataPointsIDs[dataIndex]) { var newDataIndex = splineData.MoveDataPoint(dataIndex, newIndex); //If the current DataPoint is moved across another DataPoint, then update the hotControl ID if(newDataIndex - index != 0) GUIUtility.hotControl = s_DataPointsIDs[newDataIndex]; } } } static void SplineDataHandle( int controlID, TSpline spline, SplineData splineData, float dataPointIndex, float size, out float newTime) where TSpline : ISpline { newTime = dataPointIndex; Event evt = Event.current; EventType eventType = evt.GetTypeForControl(controlID); var normalizedT = SplineUtility.GetNormalizedInterpolation(spline, dataPointIndex, splineData.PathIndexUnit); var dataPosition = SplineUtility.EvaluatePosition(spline, normalizedT); switch (eventType) { case EventType.Layout: var dist = HandleUtility.DistanceToCircle(dataPosition, size * HandleUtility.GetHandleSize(dataPosition)); HandleUtility.AddControl(controlID, dist); break; case EventType.Repaint: DrawSplineDataHandle(controlID, dataPosition, size); break; case EventType.MouseDown: if (evt.button == 0 && HandleUtility.nearestControl == controlID && GUIUtility.hotControl == 0) { GUIUtility.hotControl = controlID; newTime = GetClosestSplineDataT(spline, splineData); evt.Use(); } break; case EventType.MouseDrag: if (GUIUtility.hotControl == controlID) { newTime = GetClosestSplineDataT(spline, splineData); evt.Use(); } break; case EventType.MouseUp: if (GUIUtility.hotControl == controlID) { if(evt.button == 0) { GUIUtility.hotControl = 0; newTime = GetClosestSplineDataT(spline, splineData); } evt.Use(); } break; case EventType.MouseMove: HandleUtility.Repaint(); break; } } static void DrawSplineDataHandle(int controlID, Vector3 position, float size) { var handleColor = Handles.color; if(controlID == GUIUtility.hotControl) handleColor = Handles.selectedColor; else if(GUIUtility.hotControl == 0 && controlID == HandleUtility.nearestControl) handleColor = Handles.preselectionColor; // to avoid affecting the sphere dimensions with the handles matrix, we'll just use the position and reset // the matrix to identity when drawing. position = Handles.matrix * position; using(new Handles.DrawingScope(handleColor, Matrix4x4.identity)) { Handles.SphereHandleCap( controlID, position, Quaternion.identity, size * HandleUtility.GetHandleSize(position), EventType.Repaint ); } } // Spline must be in world space static float GetClosestSplineDataT(TSpline spline, SplineData splineData) where TSpline : ISpline { var evt = Event.current; var ray = HandleUtility.GUIPointToWorldRay(evt.mousePosition); SplineUtility.GetNearestPoint(spline, ray, out float3 _, out float t, k_PickRes); return SplineUtility.ConvertIndexUnit(spline, t, splineData.PathIndexUnit); } } }