-
-
Save Bryan-Legend/1f7f7e952be7de913e6617ce2ae8deda to your computer and use it in GitHub Desktop.
ScenePointEditor.cs - Vectroid modifications (multiselect, drag-to-place insert, editor snap, prefab override fixes)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using UnityEditor; | |
| using UnityEditor.IMGUI.Controls; | |
| using UnityEngine; | |
| using Object = UnityEngine.Object; | |
| // Shapes © Freya Holmér - https://twitter.com/FreyaHolmer/ | |
| // Website & Documentation - https://acegikmo.com/shapes/ | |
| // | |
| // MODIFIED for Vectroid: | |
| // - Removed the AddRemovePoints Tab-cycle mode entirely; add/remove now happens via | |
| // edge-hover ghost-button (Unity PolygonCollider2D-style insert) and the Delete key. | |
| // - Added multiselect (shift-click to toggle selection), Ctrl/Cmd+D to duplicate | |
| // selected points, and a combined position handle for moving multi-selections. | |
| // - hasAddRemoveMode now gates the new add/remove behavior so fixed-count shapes | |
| // (Line/Quad/Triangle) that explicitly set it to false still keep their counts. | |
| namespace Shapes { | |
| public class ScenePointEditor : SceneEditGizmos { | |
| static bool isEditing; | |
| // Public hook so EditorTool overlays (e.g. Polygon/Polyline edit tools) | |
| // can drive the same edit-mode flag the inspector "Edit Points" button does. | |
| public static bool IsEditingPoints { | |
| get => isEditing; | |
| set => isEditing = value; | |
| } | |
| public bool hasAddRemoveMode = true; | |
| public bool hasEditThicknessMode = false; | |
| public bool hasEditColorMode = false; | |
| public bool useFlatThicknessHandles = false; | |
| public event Action<ShapeRenderer, int> onValuesChanged = delegate { }; | |
| public bool[] colorEnabledArray = null; | |
| public bool[] positionEnabledArray = null; | |
| EditMode currentEditMode = EditMode.PositionHandles; | |
| readonly HashSet<int> selectedIndices = new HashSet<int>(); | |
| // Drag-from-ghost state. While the user is dragging a freshly inserted | |
| // point, insertDragControlID owns GUIUtility.hotControl and | |
| // insertDragPointIndex is the index in `points` that follows the cursor. | |
| int insertDragControlID; | |
| int insertDragPointIndex = -1; | |
| const float EDGE_HOVER_PIXEL_THRESHOLD = 12f; | |
| const float SELECT_DOT_GUI_RADIUS = 7f; | |
| // Approx screen-space reach of a Unity position handle (dot + axis arrows). | |
| // When the mouse is within this many pixels of any point, suppress the | |
| // edge-insert ghost so it can't sit under a handle and steal the click. | |
| const float POSITION_HANDLE_AVOID_PIXELS = 60f; | |
| void GoToNextEditMode() { | |
| while( true ) { | |
| currentEditMode = (EditMode)( ( (int)currentEditMode + 1 ) % (int)EditMode.COUNT ); | |
| if( CanEdit( currentEditMode ) == false ) continue; | |
| break; | |
| } | |
| } | |
| bool CanEdit( EditMode mode ) { | |
| switch( mode ) { | |
| case EditMode.EditThickness: return hasEditThicknessMode; | |
| case EditMode.EditColor: return hasEditColorMode; | |
| default: return true; | |
| } | |
| } | |
| bool HasAnyExtraMode => hasEditThicknessMode || hasEditColorMode; | |
| enum EditMode { | |
| PositionHandles, | |
| EditThickness, | |
| EditColor, | |
| COUNT | |
| } | |
| SphereBoundsHandle discHandle = ShapesHandles.InitDiscHandle(); | |
| public ScenePointEditor( Editor parentEditor ) => this.parentEditor = parentEditor; | |
| protected override bool IsEditing { | |
| get => isEditing; | |
| set => isEditing = value; | |
| } | |
| public bool DoSceneHandles( bool closed, Object component, List<PolylinePoint> points, Transform tf, float globalThicknessScale = 1f, Color globalColorTint = default ) { | |
| void SetPt( int i, Vector3 pt ) { | |
| PolylinePoint pp = points[i]; | |
| pp.point = pt; | |
| points[i] = pp; | |
| } | |
| float GetThicknessWorld( int i ) { | |
| float localThickness = points[i].thickness * globalThicknessScale; | |
| return localThickness * tf.lossyScale.AvgComponentMagnitude(); | |
| } | |
| void SetThicknessWorld( int i, float thicknessWorld ) { | |
| PolylinePoint pp = points[i]; | |
| float localThickness = thicknessWorld / tf.lossyScale.AvgComponentMagnitude(); | |
| pp.thickness = localThickness / globalThicknessScale; | |
| points[i] = pp; | |
| } | |
| Color GetNetColor( int i ) => points[i].color * globalColorTint; | |
| Color GetColor( int i ) => points[i].color; | |
| void SetColor( int i, Color color ) { | |
| PolylinePoint p = points[i]; | |
| p.color = color; | |
| points[i] = p; | |
| } | |
| return DoSceneHandles( closed, component, points, tf, i => points[i].point, p => p.point, SetPt, PolylinePoint.Lerp, GetThicknessWorld, SetThicknessWorld, GetNetColor, GetColor, SetColor, () => points.Count ); | |
| } | |
| public bool DoSceneHandles( bool closed, Object component, List<Vector2> points, Transform tf ) { | |
| return DoSceneHandles( closed, component, points, tf, i => points[i], p => p, ( i, p ) => points[i] = p, Vector2.LerpUnclamped ); | |
| } | |
| public bool DoSceneHandles( bool closed, Object component, List<Vector3> points, Transform tf ) { | |
| return DoSceneHandles( closed, component, points, tf, i => points[i], p => p, ( i, p ) => points[i] = p, Vector3.LerpUnclamped ); | |
| } | |
| public bool DoSceneHandles( bool closed, Object component, List<Vector3> points, List<Color> colors, Transform tf ) { | |
| void SetColor( int i, Color c ) { | |
| colors[i] = c; | |
| onValuesChanged( component as ShapeRenderer, i ); | |
| } | |
| return DoSceneHandles( closed, component, points, tf, i => points[i], p => p, ( i, p ) => points[i] = p, Vector3.LerpUnclamped, null, null, i => colors[i], i => colors[i], SetColor, () => colors.Count ); | |
| } | |
| bool TextureButton( Vector3 worldPos, Texture2D tex, float scale, bool fade = true ) { | |
| Rect r = new Rect( 0, 0, tex.width * scale, tex.height * scale ); | |
| r.center = HandleUtility.WorldToGUIPoint( worldPos ); | |
| Vector2 mousePos = Event.current.mousePosition; | |
| if( fade ) { | |
| float t = Mathf.InverseLerp( 200, 64, Vector2.Distance( mousePos, r.center ) ); | |
| float a = Mathf.Lerp( 0.3f, 1f, t ); | |
| GUI.color = new Color( 1, 1, 1, a ); | |
| } | |
| bool pressed = GUI.Button( r, tex, GUIStyle.none ); | |
| if( fade ) | |
| GUI.color = Color.white; | |
| return pressed; | |
| } | |
| bool DoSceneHandles<T>( bool closed, | |
| Object component, | |
| List<T> points, | |
| Transform tf, | |
| Func<int, Vector3> GetPtAt, | |
| Func<T, Vector3> GetPt, | |
| Action<int, Vector3> SetPt, | |
| Func<T, T, float, T> Lerp, | |
| Func<int, float> GetThicknessWorld = null, | |
| Action<int, float> SetThicknessWorld = null, | |
| Func<int, Color> GetNetColor = null, | |
| Func<int, Color> GetColor = null, | |
| Action<int, Color> SetColor = null, | |
| Func<int> ColorCount = null ) { | |
| CheckForCancelEditAction(); | |
| if( IsHoldingAlt ) | |
| return false; | |
| bool changed = false; | |
| Vector3 GetWorldPt( int i ) => tf.TransformPoint( GetPtAt( i ) ); | |
| if( !isEditing ) | |
| return false; | |
| // Drop selection indices that no longer exist (e.g. after external edits) | |
| selectedIndices.RemoveWhere( idx => idx < 0 || idx >= points.Count ); | |
| if( Event.current.isKey && Event.current.keyCode == KeyCode.Tab && HasAnyExtraMode ) { | |
| if( Event.current.type == EventType.KeyDown ) | |
| GoToNextEditMode(); | |
| Event.current.Use(); | |
| } | |
| // Mode label overlay | |
| if( Selection.gameObjects.Length > 0 && Selection.gameObjects[0] == ( (Component)component ).gameObject ) { | |
| if( Event.current.type == EventType.MouseMove ) | |
| SceneView.lastActiveSceneView.Repaint(); | |
| if( HasAnyExtraMode ) { | |
| Handles.BeginGUI(); | |
| Vector2 mousePos = Event.current.mousePosition; | |
| Rect r = new Rect( mousePos.x + 32, mousePos.y, Screen.width, 128 ); | |
| string label = "Press Tab to cycle modes:"; | |
| void SelectLabel( string str, EditMode mode, bool exists = true ) { | |
| if( !exists ) return; | |
| label += mode == currentEditMode ? "\n> " + str : "\n " + str; | |
| } | |
| SelectLabel( "position", EditMode.PositionHandles ); | |
| SelectLabel( "thickness", EditMode.EditThickness, hasEditThicknessMode ); | |
| SelectLabel( "color", EditMode.EditColor, hasEditColorMode ); | |
| GUI.Label( r, label ); | |
| Handles.EndGUI(); | |
| } | |
| } | |
| Quaternion handleRotation = Tools.pivotRotation == PivotRotation.Global ? Quaternion.identity : tf.rotation; | |
| if( currentEditMode == EditMode.EditThickness ) { | |
| Transform camTf = SceneView.lastActiveSceneView.camera.transform; | |
| Vector3 camPos = camTf.position; | |
| Vector3 camUp = camTf.up; | |
| for( int i = 0; i < points.Count; i++ ) { | |
| discHandle.radius = GetThicknessWorld( i ) / 2f; | |
| discHandle.center = Vector3.zero; | |
| Vector3 discPos = GetWorldPt( i ); | |
| Vector3 dirToCamera = discPos - camPos; | |
| Quaternion discRot = useFlatThicknessHandles ? tf.rotation : Quaternion.LookRotation( dirToCamera, camUp ); | |
| Matrix4x4 mtx = Matrix4x4.TRS( discPos, discRot, Vector3.one ); | |
| using( var chChk = new EditorGUI.ChangeCheckScope() ) { | |
| using( new Handles.DrawingScope( ShapesHandles.GetHandleColor( GetNetColor( i ) ), mtx ) ) | |
| discHandle.DrawHandle(); | |
| if( chChk.changed ) { | |
| changed = true; | |
| Undo.RecordObject( component, "edit thickness" ); | |
| SetThicknessWorld( i, discHandle.radius * 2 ); | |
| break; | |
| } | |
| } | |
| } | |
| } else if( currentEditMode == EditMode.EditColor ) { | |
| Handles.BeginGUI(); | |
| for( int i = 0; i < ColorCount(); i++ ) { | |
| if( colorEnabledArray != null && colorEnabledArray[i] == false ) continue; | |
| Vector3 ptWorld = GetWorldPt( i ); | |
| Color col = GetColor( i ); | |
| col.a = 1f; | |
| GUI.color = col; | |
| if( TextureButton( ptWorld, UIAssets.Instance.pointEditColor, 0.5f, fade: false ) ) { | |
| int captured = i; | |
| ShapesUI.ShowColorPicker( OnColorChanged, GetColor( i ) ); | |
| void OnColorChanged( Color c ) { | |
| Undo.RecordObject( component, "modify color" ); | |
| SetColor( captured, c ); | |
| ( component as ShapeRenderer )?.UpdateAllMaterialProperties(); | |
| ( component as ShapeRenderer )?.UpdateMesh( force: true ); | |
| // Async callback — doesn't go through the DoSceneHandles | |
| // changed-return path, so mark dirty + record overrides here. | |
| MarkComponentChanged( component ); | |
| ShapesUI.RepaintAllSceneViews(); | |
| } | |
| } | |
| } | |
| GUI.color = Color.white; | |
| Handles.EndGUI(); | |
| } else if( currentEditMode == EditMode.PositionHandles ) { | |
| changed |= DoPositionMode( closed, component, points, tf, GetWorldPt, GetPt, SetPt, Lerp ); | |
| } | |
| // Undo.RecordObject on its own isn't enough to persist changes to a | |
| // List<T>'s contents (e.g. inserting/removing/mutating points): Unity | |
| // won't pick up the modification reliably and prefab instances won't | |
| // register an override. Mark dirty + record prefab modifications here | |
| // so edits survive scene/prefab saves. | |
| if( changed ) | |
| MarkComponentChanged( component ); | |
| return changed; | |
| } | |
| static void MarkComponentChanged( Object component ) { | |
| if( component == null ) return; | |
| EditorUtility.SetDirty( component ); | |
| if( PrefabUtility.IsPartOfPrefabInstance( component ) ) | |
| PrefabUtility.RecordPrefabInstancePropertyModifications( component ); | |
| } | |
| // Project the mouse ray onto the shape's local XY plane (in world space). | |
| // 2D shapes live on tf.forward; this returns where the cursor "is" on that | |
| // plane so a drag can place the point under the mouse. | |
| static bool ProjectMouseOntoPlane( Transform tf, Vector2 mouseGui, out Vector3 worldPos ) { | |
| Plane plane = new Plane( tf.forward, tf.position ); | |
| Ray ray = HandleUtility.GUIPointToWorldRay( mouseGui ); | |
| if( plane.Raycast( ray, out float dist ) ) { | |
| worldPos = ray.GetPoint( dist ); | |
| return true; | |
| } | |
| worldPos = default; | |
| return false; | |
| } | |
| static Vector3 SnapWorldIfEnabled( Vector3 worldPos ) { | |
| if( !EditorSnapSettings.gridSnapEnabled ) return worldPos; | |
| Vector3 step = EditorSnapSettings.move; | |
| if( step.x <= 0f ) step.x = 1f; | |
| if( step.y <= 0f ) step.y = 1f; | |
| if( step.z <= 0f ) step.z = 1f; | |
| return new Vector3( | |
| Mathf.Round( worldPos.x / step.x ) * step.x, | |
| Mathf.Round( worldPos.y / step.y ) * step.y, | |
| Mathf.Round( worldPos.z / step.z ) * step.z | |
| ); | |
| } | |
| // Position-handle mode with multiselect, edge-insert, delete, duplicate. | |
| bool DoPositionMode<T>( bool closed, | |
| Object component, | |
| List<T> points, | |
| Transform tf, | |
| Func<int, Vector3> GetWorldPt, | |
| Func<T, Vector3> GetPt, | |
| Action<int, Vector3> SetPt, | |
| Func<T, T, float, T> Lerp ) { | |
| bool changed = false; | |
| Event e = Event.current; | |
| Quaternion handleRotation = Tools.pivotRotation == PivotRotation.Global ? Quaternion.identity : tf.rotation; | |
| int minPoints = closed ? 3 : 2; | |
| // --- Click-based selection (event-level) --- | |
| // The dot's Handles.Button can't see the click any more because the | |
| // always-drawn position handle's center hit area sits on top of it. Handle | |
| // shift+click ourselves before any Handles call so multiselect works. | |
| // Plain clicks near a point also retarget the selection to that point so | |
| // the dot color tracks whatever handle the user actually grabbed; the | |
| // event isn't consumed in that case so the position handle still drags. | |
| if( GUIUtility.hotControl == 0 | |
| && e.type == EventType.MouseDown | |
| && e.button == 0 ) { | |
| bool shift = ( e.modifiers & EventModifiers.Shift ) != 0; | |
| Vector2 mouseGui = e.mousePosition; | |
| int closestIdx = -1; | |
| // Tighter radius for shift-click (precise toggle) than for plain | |
| // click-to-retarget (covers the position handle's pickable center). | |
| float closestDist = shift ? SELECT_DOT_GUI_RADIUS * 2.5f : POSITION_HANDLE_AVOID_PIXELS; | |
| for( int i = 0; i < points.Count; i++ ) { | |
| if( positionEnabledArray != null && positionEnabledArray[i] == false ) continue; | |
| Vector2 gp = HandleUtility.WorldToGUIPoint( GetWorldPt( i ) ); | |
| float d = Vector2.Distance( mouseGui, gp ); | |
| if( d < closestDist ) { | |
| closestDist = d; | |
| closestIdx = i; | |
| } | |
| } | |
| if( closestIdx >= 0 ) { | |
| if( shift ) { | |
| if( !selectedIndices.Add( closestIdx ) ) | |
| selectedIndices.Remove( closestIdx ); | |
| e.Use(); | |
| } else if( selectedIndices.Count <= 1 ) { | |
| // Skip when multi-select is active so clicking the centroid | |
| // handle doesn't collapse the selection. | |
| selectedIndices.Clear(); | |
| selectedIndices.Add( closestIdx ); | |
| // Don't Use() — let the position handle take the drag. | |
| } | |
| SceneView.RepaintAll(); | |
| } | |
| } | |
| // --- Edge-hover ghost (insert) --- | |
| // Suppressed entirely while a multi-select is active: the centroid handle | |
| // sits at the midpoint of two selected adjacent points, exactly where the | |
| // ghost would render, and it would steal the centroid handle's click. | |
| int hoverEdgeStart = -1; | |
| Vector3 hoverInsertWorld = default; | |
| float hoverInsertT = 0f; | |
| if( hasAddRemoveMode && points.Count >= 2 && selectedIndices.Count <= 1 ) { | |
| Vector2 mouseGui = e.mousePosition; | |
| // Suppress ghost if the mouse is anywhere near a point's position-handle | |
| // territory. Without this, an edge that passes under a handle (or runs | |
| // along one of its axis arrows) lets the ghost get drawn directly under | |
| // the cursor, where Handles.Button steals the click. | |
| bool nearAnyPoint = false; | |
| for( int k = 0; k < points.Count; k++ ) { | |
| if( positionEnabledArray != null && positionEnabledArray[k] == false ) continue; | |
| Vector2 gp = HandleUtility.WorldToGUIPoint( GetWorldPt( k ) ); | |
| if( Vector2.Distance( mouseGui, gp ) < POSITION_HANDLE_AVOID_PIXELS ) { | |
| nearAnyPoint = true; | |
| break; | |
| } | |
| } | |
| if( !nearAnyPoint ) { | |
| float bestDist = EDGE_HOVER_PIXEL_THRESHOLD; | |
| int edgeCount = closed ? points.Count : points.Count - 1; | |
| for( int i = 0; i < edgeCount; i++ ) { | |
| int j = ( i + 1 ) % points.Count; | |
| Vector3 wa = GetWorldPt( i ); | |
| Vector3 wb = GetWorldPt( j ); | |
| Vector2 ga = HandleUtility.WorldToGUIPoint( wa ); | |
| Vector2 gb = HandleUtility.WorldToGUIPoint( wb ); | |
| Vector2 ab = gb - ga; | |
| float lenSq = ab.sqrMagnitude; | |
| if( lenSq < 0.0001f ) continue; | |
| float t = Mathf.Clamp01( Vector2.Dot( mouseGui - ga, ab ) / lenSq ); | |
| Vector2 closestGui = ga + t * ab; | |
| // Skip if closer to either endpoint (those are point selectors) | |
| if( Vector2.Distance( closestGui, ga ) < SELECT_DOT_GUI_RADIUS * 1.5f ) continue; | |
| if( Vector2.Distance( closestGui, gb ) < SELECT_DOT_GUI_RADIUS * 1.5f ) continue; | |
| float dist = Vector2.Distance( mouseGui, closestGui ); | |
| if( dist < bestDist ) { | |
| bestDist = dist; | |
| hoverEdgeStart = i; | |
| hoverInsertT = t; | |
| hoverInsertWorld = Vector3.LerpUnclamped( wa, wb, t ); | |
| } | |
| } | |
| } | |
| } | |
| // --- Selection dots (per point, click to select / shift-click to toggle) --- | |
| for( int i = 0; i < points.Count; i++ ) { | |
| if( positionEnabledArray != null && positionEnabledArray[i] == false ) continue; | |
| Vector3 ptWorld = GetWorldPt( i ); | |
| bool sel = selectedIndices.Contains( i ); | |
| float handleSize = HandleUtility.GetHandleSize( ptWorld ) * 0.06f; | |
| Color dotColor = sel ? new Color( 1f, 0.7f, 0.1f ) : new Color( 0.2f, 0.6f, 1f ); | |
| using( new Handles.DrawingScope( dotColor ) ) { | |
| if( Handles.Button( ptWorld, Quaternion.identity, handleSize, handleSize * 1.6f, Handles.DotHandleCap ) ) { | |
| bool shift = ( e.modifiers & EventModifiers.Shift ) != 0; | |
| if( shift ) { | |
| if( !selectedIndices.Add( i ) ) | |
| selectedIndices.Remove( i ); | |
| } else { | |
| selectedIndices.Clear(); | |
| selectedIndices.Add( i ); | |
| } | |
| SceneView.RepaintAll(); | |
| } | |
| } | |
| } | |
| // --- Drag of just-inserted point (ghost MouseDown handed us hotControl) --- | |
| // Process the drag BEFORE the position handles draw so each handle reads | |
| // the freshly-set position on the same frame. | |
| if( insertDragControlID != 0 && GUIUtility.hotControl == insertDragControlID ) { | |
| if( insertDragPointIndex >= 0 && insertDragPointIndex < points.Count ) { | |
| if( e.type == EventType.MouseDrag ) { | |
| Vector3 worldPos; | |
| if( ProjectMouseOntoPlane( tf, e.mousePosition, out worldPos ) ) { | |
| worldPos = SnapWorldIfEnabled( worldPos ); | |
| Undo.RecordObject( component, "drag inserted point" ); | |
| SetPt( insertDragPointIndex, tf.InverseTransformPoint( worldPos ) ); | |
| changed = true; | |
| e.Use(); | |
| } | |
| } else if( e.type == EventType.MouseUp && e.button == 0 ) { | |
| GUIUtility.hotControl = 0; | |
| insertDragControlID = 0; | |
| insertDragPointIndex = -1; | |
| e.Use(); | |
| } | |
| } else { | |
| // Index went out of range (undo, external edit) — bail safely. | |
| GUIUtility.hotControl = 0; | |
| insertDragControlID = 0; | |
| insertDragPointIndex = -1; | |
| } | |
| } | |
| // --- Movement handles --- | |
| if( selectedIndices.Count > 1 ) { | |
| // Combined handle at centroid of selected | |
| Vector3 centroid = Vector3.zero; | |
| foreach( int idx in selectedIndices ) centroid += GetWorldPt( idx ); | |
| centroid /= selectedIndices.Count; | |
| EditorGUI.BeginChangeCheck(); | |
| Vector3 newCentroid = Handles.PositionHandle( centroid, handleRotation ); | |
| if( EditorGUI.EndChangeCheck() ) { | |
| Vector3 deltaWorld = newCentroid - centroid; | |
| if( deltaWorld.sqrMagnitude > 0f ) { | |
| Undo.RecordObject( component, "move points" ); | |
| foreach( int idx in selectedIndices ) { | |
| Vector3 nw = GetWorldPt( idx ) + deltaWorld; | |
| SetPt( idx, tf.InverseTransformPoint( nw ) ); | |
| } | |
| changed = true; | |
| } | |
| } | |
| } else { | |
| // Always draw a position handle on every point (when 0 or 1 selected) so the | |
| // handle count — and therefore the ControlIDs Handles.PositionHandle allocates | |
| // — stays stable frame-to-frame. Filtering by selection here caused Unity's | |
| // hot control to map to the wrong index when the count changed, which made a | |
| // drag "grab the last point I was moving." | |
| for( int i = 0; i < points.Count; i++ ) { | |
| if( positionEnabledArray != null && positionEnabledArray[i] == false ) continue; | |
| Vector3 ptWorld = GetWorldPt( i ); | |
| EditorGUI.BeginChangeCheck(); | |
| Vector3 newPosWorld = Handles.PositionHandle( ptWorld, handleRotation ); | |
| if( EditorGUI.EndChangeCheck() ) { | |
| changed = true; | |
| Undo.RecordObject( component, "modify points" ); | |
| SetPt( i, tf.InverseTransformPoint( newPosWorld ) ); | |
| // Retarget selection to whichever point was actually dragged — | |
| // covers the case where the click lands on an axis arrow tip | |
| // far from the point center, outside the MouseDown radius. | |
| selectedIndices.Clear(); | |
| selectedIndices.Add( i ); | |
| } | |
| } | |
| } | |
| // --- Edge insert ghost (manual control) --- | |
| // Drawn AFTER the position handles so its conditional ControlID can't | |
| // shift theirs. We don't use Handles.Button here because that only | |
| // fires on click-release; we want MouseDown to insert the point AND | |
| // claim hotControl so the user can drag the new point straight to its | |
| // final position and release it on MouseUp. | |
| if( hoverEdgeStart >= 0 && insertDragControlID == 0 ) { | |
| int ghostControlID = GUIUtility.GetControlID( FocusType.Passive ); | |
| float ghostSize = HandleUtility.GetHandleSize( hoverInsertWorld ) * 0.06f; | |
| if( e.type == EventType.Repaint ) { | |
| using( new Handles.DrawingScope( new Color( 0.4f, 1f, 0.4f, 0.8f ) ) ) | |
| Handles.SphereHandleCap( ghostControlID, hoverInsertWorld, Quaternion.identity, ghostSize, EventType.Repaint ); | |
| } else if( e.type == EventType.MouseDown && e.button == 0 ) { | |
| Vector2 ghostGui = HandleUtility.WorldToGUIPoint( hoverInsertWorld ); | |
| if( Vector2.Distance( e.mousePosition, ghostGui ) <= EDGE_HOVER_PIXEL_THRESHOLD ) { | |
| Undo.RecordObject( component, "insert point" ); | |
| T newPt = Lerp( points[hoverEdgeStart], points[( hoverEdgeStart + 1 ) % points.Count], hoverInsertT ); | |
| int insertAt = hoverEdgeStart + 1; | |
| points.Insert( insertAt, newPt ); | |
| // Initial position: the ghost world position, snapped if the | |
| // magnet is on. The drag handler re-snaps each MouseDrag. | |
| Vector3 startWorld = SnapWorldIfEnabled( hoverInsertWorld ); | |
| SetPt( insertAt, tf.InverseTransformPoint( startWorld ) ); | |
| selectedIndices.Clear(); | |
| selectedIndices.Add( insertAt ); | |
| insertDragControlID = ghostControlID; | |
| insertDragPointIndex = insertAt; | |
| GUIUtility.hotControl = ghostControlID; | |
| e.Use(); | |
| changed = true; | |
| } | |
| } | |
| // Repaint while moving so the ghost tracks the mouse smoothly | |
| if( e.type == EventType.MouseMove ) | |
| SceneView.lastActiveSceneView.Repaint(); | |
| } | |
| // --- Keyboard shortcuts --- | |
| if( e.type == EventType.KeyDown && hasAddRemoveMode && selectedIndices.Count > 0 ) { | |
| bool ctrl = ( e.modifiers & ( EventModifiers.Control | EventModifiers.Command ) ) != 0; | |
| if( e.keyCode == KeyCode.Delete || e.keyCode == KeyCode.Backspace ) { | |
| int allowedRemovals = Mathf.Max( 0, points.Count - minPoints ); | |
| if( allowedRemovals > 0 ) { | |
| Undo.RecordObject( component, "delete points" ); | |
| var toRemove = selectedIndices.OrderByDescending( x => x ).Take( allowedRemovals ).ToList(); | |
| foreach( int idx in toRemove ) | |
| points.RemoveAt( idx ); | |
| selectedIndices.Clear(); | |
| changed = true; | |
| } | |
| e.Use(); | |
| SceneView.RepaintAll(); | |
| } else if( ctrl && e.keyCode == KeyCode.D ) { | |
| Undo.RecordObject( component, "duplicate points" ); | |
| var sortedAsc = selectedIndices.OrderBy( x => x ).ToList(); | |
| // Insert highest-first so earlier indices don't shift | |
| for( int i = sortedAsc.Count - 1; i >= 0; i-- ) { | |
| int idx = sortedAsc[i]; | |
| points.Insert( idx + 1, points[idx] ); | |
| } | |
| selectedIndices.Clear(); | |
| for( int i = 0; i < sortedAsc.Count; i++ ) | |
| selectedIndices.Add( sortedAsc[i] + 1 + i ); | |
| changed = true; | |
| e.Use(); | |
| SceneView.RepaintAll(); | |
| } else if( e.keyCode == KeyCode.A && ctrl ) { | |
| selectedIndices.Clear(); | |
| for( int i = 0; i < points.Count; i++ ) | |
| selectedIndices.Add( i ); | |
| e.Use(); | |
| SceneView.RepaintAll(); | |
| } | |
| } | |
| return changed; | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment