From df8477862017f5192b30dcfcd73f381d9e8d023f Mon Sep 17 00:00:00 2001 From: Sachin Date: Mon, 25 May 2026 01:07:35 +0530 Subject: [PATCH 01/13] cut tool rectangle mode --- Editor/EditorCore/CutTool.cs | 243 ++++++++++++++++++++++++++++++++++- 1 file changed, 237 insertions(+), 6 deletions(-) diff --git a/Editor/EditorCore/CutTool.cs b/Editor/EditorCore/CutTool.cs index 7f44d05fe..7474b1aec 100644 --- a/Editor/EditorCore/CutTool.cs +++ b/Editor/EditorCore/CutTool.cs @@ -127,6 +127,15 @@ public override GUIContent toolbarIcon const string k_SnapToGeometryPrefKey = "VertexInsertion.snapToGeometry"; const string k_SnappingDistancePrefKey = "VertexInsertion.snappingDistance"; + //Rectangle mode fields + bool m_RectangleMode; + Vector3 m_RectStartPoint = Vector3.positiveInfinity; + Vector3 m_RectEndPoint = Vector3.positiveInfinity; + bool m_RectDragging; + const string k_RectangleModePrefKey = "VertexInsertion.rectangleMode"; + static readonly Color k_RectPreviewColor = new Color(1f, 1f, 0f, 0.4f); + static readonly Color k_RectOutlineColor = new Color(1f, 1f, 0f, 1f); + public bool isALoop { get @@ -182,6 +191,7 @@ void OnEnable() m_OverlayTitle = new GUIContent("Cut Settings"); m_SnapToGeometry = EditorPrefs.GetBool( k_SnapToGeometryPrefKey, false ); m_SnappingDistance = EditorPrefs.GetFloat( k_SnappingDistancePrefKey, 0.1f ); + m_RectangleMode = EditorPrefs.GetBool( k_RectangleModePrefKey, false ); m_CutCursorTexture = IconUtility.GetIcon("Cursors/cutCursor"); m_CutAddCursorTexture = IconUtility.GetIcon("Cursors/cutCursor-add"); @@ -205,7 +215,8 @@ public override void OnActivated() public override void OnWillBeDeactivated() { - ExecuteCut(false); + if(!m_RectangleMode) + ExecuteCut(false); Undo.undoRedoPerformed -= UndoRedoPerformed; MeshSelection.objectSelectionChanged -= UpdateTarget; @@ -225,6 +236,10 @@ void Clear() m_CutPath.Clear(); m_MeshConnections.Clear(); + m_RectDragging = false; + m_RectStartPoint = Vector3.positiveInfinity; + m_RectEndPoint = Vector3.positiveInfinity; + m_SelectedVertices = null; m_SelectedEdges = null; @@ -338,7 +353,10 @@ public override void OnToolGUI( EditorWindow window ) if(currentEvent.type == EventType.Layout) HandleUtility.AddDefaultControl(m_ControlId); - DoPointPlacement(window); + if(m_RectangleMode) + DoRectanglePlacement(window); + else + DoPointPlacement(window); //Refresh the cut shape if points have been added to it or removed. if(m_Dirty) @@ -346,7 +364,11 @@ public override void OnToolGUI( EditorWindow window ) if(currentEvent.type == EventType.Repaint) { - DrawGuideLine(); + if(m_RectangleMode) + DoRectanglePreview(); + else + DrawGuideLine(); + DoCurrentPointsGUI(); DoVisualCues(); @@ -393,10 +415,26 @@ void OnOverlayGUI(UObject target, SceneView view) GUI.enabled = MeshSelection.selectedObjectCount == 1; - m_SnapToGeometry = DoOverlayToggle(L10n.Tr("Snap to existing edges and vertices"), m_SnapToGeometry); - EditorPrefs.SetBool(k_SnapToGeometryPrefKey, m_SnapToGeometry); + m_RectangleMode = DoOverlayToggle(L10n.Tr("Rectangle Mode"), m_RectangleMode); + EditorPrefs.SetBool(k_RectangleModePrefKey, m_RectangleMode); + + if(m_RectangleMode) + { + EditorGUI.indentLevel++; + using(new GUILayout.HorizontalScope()) + { + EditorGUILayout.LabelField(L10n.Tr("Click & drag to draw a rectangle cut"), GUILayout.Width(250)); + } + EditorGUI.indentLevel--; + } + else + { + m_SnapToGeometry = DoOverlayToggle(L10n.Tr("Snap to existing edges and vertices"), m_SnapToGeometry); + EditorPrefs.SetBool(k_SnapToGeometryPrefKey, m_SnapToGeometry); + + } - if(!m_SnapToGeometry) + if(!m_RectangleMode && !m_SnapToGeometry) GUI.enabled = false; EditorGUI.indentLevel++; using(new GUILayout.HorizontalScope()) @@ -573,6 +611,199 @@ void DoPointPlacement(EditorWindow window) } } + /// + /// Rectangle mode: click and drag to define a rectangular cut on the face. + /// On mouse up, auto-places the 4 corners and executes the cut. + /// + void DoRectanglePlacement(EditorWindow window) + { + Event evt = Event.current; + EventType evtType = evt.type; + + bool hasHitPosition = UpdateHitPosition(); + + // Visual helpers + if (evtType == EventType.Repaint) + { + m_SnappingPoint = m_SnapToGeometry || (evt.modifiers & EventModifiers.Control) != 0; + m_ModifyingPoint = false; + + if (hasHitPosition && IsCursorInSceneView(window)) + { + m_CurrentCutCursor = m_CutCursorTexture; + m_CurrentHandleColor = k_HandleColorAddNewVertex; + } + else + { + m_CurrentCutCursor = null; + m_CurrentPosition = Vector3.positiveInfinity; + } + } + + // Mouse down: start rectangle drag + if (hasHitPosition + && evtType == EventType.MouseDown && evt.button == 0 + && HandleUtility.nearestControl == m_ControlId + && !m_RectDragging) + { + m_RectDragging = true; + m_RectStartPoint = m_CurrentPosition; + m_RectEndPoint = m_CurrentPosition; + m_TargetFace = m_CurrentFace; + + var edges = m_TargetFace.edges; + m_SelectedVertices = edges.Select(e => e.a).ToArray(); + m_SelectedEdges = edges.ToArray(); + + m_CutPath.Clear(); + m_MeshConnections.Clear(); + evt.Use(); + } + + // Mouse drag: update rectangle end point + if (m_RectDragging && evtType == EventType.MouseDrag && evt.button == 0) + { + if (hasHitPosition && m_CurrentFace == m_TargetFace) + { + m_RectEndPoint = m_CurrentPosition; + } + evt.Use(); + } + + // Mouse up: finalize the rectangle and execute cut + if (m_RectDragging + && (evtType == EventType.MouseUp && evt.button == 0)) + { + m_RectDragging = false; + + // Project start/end onto the face plane to compute the other 2 corners + Vector3 start = m_RectStartPoint; + Vector3 end = m_RectEndPoint; + + // Compute face normal for projection + Vector3 faceNormal = Math.Normal(m_Mesh, m_TargetFace); + + // Create a local coordinate system on the face plane + Vector3 faceRight, faceUp; + if (Mathf.Abs(Vector3.Dot(faceNormal, Vector3.up)) > 0.99f) + faceRight = Vector3.Cross(faceNormal, Vector3.forward).normalized; + else + faceRight = Vector3.Cross(faceNormal, Vector3.up).normalized; + faceUp = Vector3.Cross(faceNormal, faceRight).normalized; + + // Decompose rect diagonals in face space + Vector3 diagonal = end - start; + float rightDot = Vector3.Dot(diagonal, faceRight); + float upDot = Vector3.Dot(diagonal, faceUp); + + // Compute the 4 rectangle corners in world space + Vector3 corner0 = start; + Vector3 corner1 = start + faceRight * rightDot; + Vector3 corner2 = end; + Vector3 corner3 = start + faceUp * upDot; + + if (HasSignificantRectangle(corner0, corner2)) + { + // Build cut path: 4 corners + close back to start to form a loop + UndoUtility.RecordObject(this, "Rectangle Cut"); + + m_CurrentPosition = corner0; + m_CurrentPositionNormal = faceNormal; + m_CurrentVertexTypes = VertexTypes.NewVertex; + m_CurrentFace = m_TargetFace; + AddCurrentPositionToPath(false); + + m_CurrentPosition = corner1; + m_CurrentPositionNormal = faceNormal; + m_CurrentVertexTypes = VertexTypes.NewVertex; + AddCurrentPositionToPath(false); + + m_CurrentPosition = corner2; + m_CurrentPositionNormal = faceNormal; + m_CurrentVertexTypes = VertexTypes.NewVertex; + AddCurrentPositionToPath(false); + + m_CurrentPosition = corner3; + m_CurrentPositionNormal = faceNormal; + m_CurrentVertexTypes = VertexTypes.NewVertex; + AddCurrentPositionToPath(false); + + // Close the loop by returning to the start corner + m_CurrentPosition = corner0; + m_CurrentPositionNormal = faceNormal; + m_CurrentVertexTypes = VertexTypes.VertexInShape; + m_CurrentFace = m_TargetFace; + AddCurrentPositionToPath(false); + + RebuildCutShape(true); + + // Execute the cut immediately + ActionResult result = DoCut(); + EditorUtility.ShowNotification(result.notification); + + Clear(); + } + + m_RectStartPoint = Vector3.positiveInfinity; + m_RectEndPoint = Vector3.positiveInfinity; + evt.Use(); + } + + // Pass through to ProBuilder selection if not interacting + if(!m_RectDragging + && m_CutPath.Count == 0 + && !hasHitPosition + && HandleUtility.nearestControl == m_ControlId) + { + ProBuilderEditor.instance.HandleMouseEvent(SceneView.lastActiveSceneView, m_ControlId); + } + } + + bool HasSignificantRectangle(Vector3 start, Vector3 end) + { + return Vector3.Distance(start, end) > 0.001f; + } + + /// + /// Draw the rectangle preview during a drag operation. + /// + void DoRectanglePreview() + { + if (!m_RectDragging || m_Mesh == null || m_TargetFace == null) + return; + + Transform trs = m_Mesh.transform; + Vector3 startW = trs.TransformPoint(m_RectStartPoint); + Vector3 endW = trs.TransformPoint(m_RectEndPoint); + + // Compute face plane + Vector3 faceNormal = Math.Normal(m_Mesh, m_TargetFace); + + Vector3 faceRight, faceUp; + if (Mathf.Abs(Vector3.Dot(faceNormal, Vector3.up)) > 0.99f) + faceRight = Vector3.Cross(faceNormal, Vector3.forward).normalized; + else + faceRight = Vector3.Cross(faceNormal, Vector3.up).normalized; + faceUp = Vector3.Cross(faceNormal, faceRight).normalized; + + Vector3 diagonal = endW - startW; + float rightDot = Vector3.Dot(diagonal, faceRight); + float upDot = Vector3.Dot(diagonal, faceUp); + + Vector3 c0 = startW; + Vector3 c1 = startW + faceRight * rightDot; + Vector3 c2 = endW; + Vector3 c3 = startW + faceUp * upDot; + + // Draw filled rectangle + Handles.color = k_RectPreviewColor; + Handles.DrawAAConvexPolygon(new Vector3[] { c0, c1, c2, c3 }); + + // Draw outline + Handles.color = k_RectOutlineColor; + Handles.DrawAAPolyLine(2f, new Vector3[] { c0, c1, c2, c3, c0 }); + } + bool CanAppendCurrentPointToPath() { int polyCount = m_CutPath.Count; From 93988b9e48757b14402f96c2842221b818790c16 Mon Sep 17 00:00:00 2001 From: Sachin Date: Mon, 25 May 2026 01:38:42 +0530 Subject: [PATCH 02/13] grid snap --- Editor/EditorCore/CutTool.cs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Editor/EditorCore/CutTool.cs b/Editor/EditorCore/CutTool.cs index 7474b1aec..3204ef458 100644 --- a/Editor/EditorCore/CutTool.cs +++ b/Editor/EditorCore/CutTool.cs @@ -120,6 +120,7 @@ public override GUIContent toolbarIcon Edge m_SnapedEdge = Edge.Empty; bool m_SnapToGeometry; + bool m_SnapToGrid; float m_SnappingDistance; //Overlay fields @@ -132,6 +133,7 @@ public override GUIContent toolbarIcon Vector3 m_RectStartPoint = Vector3.positiveInfinity; Vector3 m_RectEndPoint = Vector3.positiveInfinity; bool m_RectDragging; + const string k_SnapToGridPrefKey = "VertexInsertion.snapToGrid"; const string k_RectangleModePrefKey = "VertexInsertion.rectangleMode"; static readonly Color k_RectPreviewColor = new Color(1f, 1f, 0f, 0.4f); static readonly Color k_RectOutlineColor = new Color(1f, 1f, 0f, 1f); @@ -171,6 +173,8 @@ Texture2D cursorTexture } } + public override bool gridSnapEnabled => true; + public override bool IsAvailable() { return MeshSelection.selectedObjectCount == 1; @@ -190,6 +194,7 @@ void OnEnable() m_OverlayTitle = new GUIContent("Cut Settings"); m_SnapToGeometry = EditorPrefs.GetBool( k_SnapToGeometryPrefKey, false ); + m_SnapToGrid = EditorPrefs.GetBool( k_SnapToGridPrefKey, false ); m_SnappingDistance = EditorPrefs.GetFloat( k_SnappingDistancePrefKey, 0.1f ); m_RectangleMode = EditorPrefs.GetBool( k_RectangleModePrefKey, false ); @@ -418,6 +423,9 @@ void OnOverlayGUI(UObject target, SceneView view) m_RectangleMode = DoOverlayToggle(L10n.Tr("Rectangle Mode"), m_RectangleMode); EditorPrefs.SetBool(k_RectangleModePrefKey, m_RectangleMode); + m_SnapToGrid = DoOverlayToggle(L10n.Tr("Snap to Grid"), m_SnapToGrid); + EditorPrefs.SetBool(k_SnapToGridPrefKey, m_SnapToGrid); + if(m_RectangleMode) { EditorGUI.indentLevel++; @@ -702,6 +710,15 @@ void DoRectanglePlacement(EditorWindow window) Vector3 corner2 = end; Vector3 corner3 = start + faceUp * upDot; + // Snap all corners to grid if enabled + if (m_SnapToGrid && EditorSnapSettings.gridSnapActive) + { + corner0 = ProBuilderSnapping.Snap(corner0, EditorSnapping.activeMoveSnapValue); + corner1 = ProBuilderSnapping.Snap(corner1, EditorSnapping.activeMoveSnapValue); + corner2 = ProBuilderSnapping.Snap(corner2, EditorSnapping.activeMoveSnapValue); + corner3 = ProBuilderSnapping.Snap(corner3, EditorSnapping.activeMoveSnapValue); + } + if (HasSignificantRectangle(corner0, corner2)) { // Build cut path: 4 corners + close back to start to form a loop @@ -880,6 +897,20 @@ internal void UpdateCurrentPosition(Face face, Vector3 position, Vector3 normal) if (m_CurrentVertexTypes == VertexTypes.None && !m_ModifyingPoint) CheckPointInMesh(); + + // Only apply grid snap if the point is a free-floating new vertex on the face + // (don't pull already-snapped edge/vertex points off their targets) + if (m_CurrentVertexTypes == VertexTypes.NewVertex || m_CurrentVertexTypes == VertexTypes.None) + ApplyGridSnap(); + } + + /// + /// Snap the current position to Unity's grid if grid snapping is enabled. + /// + void ApplyGridSnap() + { + if (m_SnapToGrid && EditorSnapSettings.gridSnapActive) + m_CurrentPosition = ProBuilderSnapping.Snap(m_CurrentPosition, EditorSnapping.activeMoveSnapValue); } /// From 6109554c9a33f0209f1575abb2f0b0e52aea0e1b Mon Sep 17 00:00:00 2001 From: Sachin Date: Mon, 25 May 2026 21:08:39 +0530 Subject: [PATCH 03/13] fix cut tool --- Editor/EditorCore/CutTool.cs | 116 +++++++++++++++++++++++++++-------- 1 file changed, 90 insertions(+), 26 deletions(-) diff --git a/Editor/EditorCore/CutTool.cs b/Editor/EditorCore/CutTool.cs index 3204ef458..247d870ce 100644 --- a/Editor/EditorCore/CutTool.cs +++ b/Editor/EditorCore/CutTool.cs @@ -752,11 +752,15 @@ void DoRectanglePlacement(EditorWindow window) m_CurrentFace = m_TargetFace; AddCurrentPositionToPath(false); - RebuildCutShape(true); + // RebuildCutShape(true) already executes the cut if the path is valid + // Don't call DoCut() again — it would run on already-cleared data + RebuildCutShape(false); - // Execute the cut immediately - ActionResult result = DoCut(); - EditorUtility.ShowNotification(result.notification); + if (m_CutPath.Count >= 2 && m_IsCutValid) + { + ActionResult result = DoCut(); + EditorUtility.ShowNotification(result.notification); + } Clear(); } @@ -1103,11 +1107,12 @@ internal ActionResult DoCut() if (isALoop) { Face f = m_Mesh.CreatePolygon(cutIndexes, false); - f.submeshIndex = m_TargetFace.submeshIndex; if(f == null) return new ActionResult(ActionResult.Status.Failure, L10n.Tr("Cut Shape is not valid")); + f.submeshIndex = m_TargetFace.submeshIndex; + Vector3 nrm = Math.Normal(m_Mesh, f); Vector3 targetNrm = Math.Normal(m_Mesh, m_TargetFace); // If the shape is define in the wrong orientation compared to the former face, reverse it @@ -1213,10 +1218,12 @@ List ComputeNewFaces(Face face, IList cutVertexIndexes) { List toDelete; Face newFace = ComputeFaceClosure(polygon, index, cutVertexSharedIndexes, out toDelete); - newFace.submeshIndex = m_TargetFace.submeshIndex; - - newFaces.Add(newFace); - facesToDelete.AddRange(toDelete); + if (newFace != null && newFace.indexesInternal != null) + { + newFace.submeshIndex = m_TargetFace.submeshIndex; + newFaces.Add(newFace); + facesToDelete.AddRange(toDelete); + } } //Start a new polygon @@ -1251,10 +1258,26 @@ Face ComputeFaceClosure( List polygonStart, int currentIndex, List cut IList uniqueIdToVertexIndex = m_Mesh.sharedVertices; Dictionary sharedToUnique = m_Mesh.sharedVertexLookup; + facesToDelete = new List(); + + if (polygonStart == null || polygonStart.Count == 0 || cutIndexes == null || cutIndexes.Count == 0) + return null; + int polygonFirstVertex = polygonStart[0]; int startIndex = cutIndexes.IndexOf(currentIndex); - SimpleTuple connection = m_MeshConnections.Find(tup => sharedToUnique[tup.item2] == sharedToUnique[polygonFirstVertex]); + if (startIndex < 0 || !sharedToUnique.ContainsKey(polygonFirstVertex)) + return null; + + int polygonFirstSharedIndex = sharedToUnique[polygonFirstVertex]; + + bool hasConnection = m_MeshConnections.Exists(tup => sharedToUnique.ContainsKey(tup.item2) + && sharedToUnique[tup.item2] == polygonFirstSharedIndex); + SimpleTuple connection = default; + + if (hasConnection) + connection = m_MeshConnections.Find(tup => sharedToUnique.ContainsKey(tup.item2) + && sharedToUnique[tup.item2] == polygonFirstSharedIndex); List> closureCandidates = new List>(); @@ -1267,8 +1290,9 @@ Face ComputeFaceClosure( List polygonStart, int currentIndex, List cut { int vertexIndex = uniqueIdToVertexIndex[cutIndexes[(index + cutIndexes.Count) % cutIndexes.Count]][0]; candidate.Add(vertexIndex); - if(sharedToUnique[vertexIndex] == sharedToUnique[polygonFirstVertex] || - sharedToUnique[vertexIndex] == sharedToUnique[connection.item1]) + if(sharedToUnique[vertexIndex] == polygonFirstSharedIndex || + (hasConnection && sharedToUnique.ContainsKey(connection.item1) + && sharedToUnique[vertexIndex] == sharedToUnique[connection.item1])) { connected = true; break; @@ -1287,8 +1311,9 @@ Face ComputeFaceClosure( List polygonStart, int currentIndex, List cut { int vertexIndex = uniqueIdToVertexIndex[cutIndexes[index % cutIndexes.Count]][0]; candidate.Add(vertexIndex); - if(sharedToUnique[vertexIndex] == sharedToUnique[polygonFirstVertex] || - sharedToUnique[vertexIndex] == sharedToUnique[connection.item1]) + if(sharedToUnique[vertexIndex] == polygonFirstSharedIndex || + (hasConnection && sharedToUnique.ContainsKey(connection.item1) + && sharedToUnique[vertexIndex] == sharedToUnique[connection.item1])) { connected = true; break; @@ -1300,7 +1325,6 @@ Face ComputeFaceClosure( List polygonStart, int currentIndex, List cut closureCandidates.Add(candidate); //Go through the different candidate and keep the best one - facesToDelete = new List(); Face bestFace = null; float bestArea = 0f; foreach(var closure in closureCandidates) @@ -1308,15 +1332,22 @@ Face ComputeFaceClosure( List polygonStart, int currentIndex, List cut closure.AddRange(polygonStart); Face face = m_Mesh.CreatePolygon(closure, false); + meshVertices.Clear(); m_Mesh.GetVerticesInList(meshVertices); uniqueIdToVertexIndex = m_Mesh.sharedVertices; sharedToUnique = m_Mesh.sharedVertexLookup; + float area; + if (!TryGetFaceArea(face, meshVertices, uniqueIdToVertexIndex, sharedToUnique, out area)) + { + if (face != null) + facesToDelete.Add(face); + + continue; + } + if(bestFace != null) { - Vector3[] vertices = meshVertices.Select(vertex => vertex.position).ToArray(); - int[] indexes = face.indexesInternal.Select(i => uniqueIdToVertexIndex[sharedToUnique[i]][0]).ToArray(); - float area = Math.PolygonArea(vertices, indexes); if(area < bestArea) { facesToDelete.Add(bestFace); @@ -1329,18 +1360,44 @@ Face ComputeFaceClosure( List polygonStart, int currentIndex, List cut else { bestFace = face; - if (face.indexesInternal != null) - { - Vector3[] vertices = meshVertices.Select(vertex => vertex.position).ToArray(); - int[] indexes = face.indexesInternal.Select(i => uniqueIdToVertexIndex[sharedToUnique[i]][0]).ToArray(); - bestArea = Math.PolygonArea(vertices, indexes); - } + bestArea = area; } } return bestFace; } + bool TryGetFaceArea(Face face, List meshVertices, IList uniqueIdToVertexIndex, + Dictionary sharedToUnique, out float area) + { + area = 0f; + + if (face == null || face.indexesInternal == null) + return false; + + Vector3[] vertices = meshVertices.Select(vertex => vertex.position).ToArray(); + int[] indexes = new int[face.indexesInternal.Length]; + + for (int i = 0; i < face.indexesInternal.Length; i++) + { + int uniqueIndex; + if (!sharedToUnique.TryGetValue(face.indexesInternal[i], out uniqueIndex)) + return false; + + if (uniqueIndex < 0 || uniqueIndex >= uniqueIdToVertexIndex.Count) + return false; + + SharedVertex sharedVertex = uniqueIdToVertexIndex[uniqueIndex]; + if (sharedVertex == null || sharedVertex.Count < 1) + return false; + + indexes[i] = sharedVertex[0]; + } + + area = Math.PolygonArea(vertices, indexes); + return true; + } + /// /// Check whether the current position (m_CurrentPosition) can be associated/snapped to an existing position of the path /// @@ -1818,11 +1875,18 @@ void DrawGuideLine() /// void DrawMeshConnectionsHandles() { - if(m_MeshConnections.Count > 0) + if(m_MeshConnections.Count > 0 && m_Mesh != null) { Vertex[] vertices = m_Mesh.GetVertices(); - foreach(var connection in m_MeshConnections) + for (int i = m_MeshConnections.Count - 1; i >= 0; i--) { + var connection = m_MeshConnections[i]; + if (connection.item1 < 0 || connection.item1 >= m_CutPath.Count + || connection.item2 < 0 || connection.item2 >= vertices.Length) + { + m_MeshConnections.RemoveAt(i); + continue; + } Handles.color = k_ConnectionsLineColor; Handles.DrawDottedLine(m_Mesh.transform.TransformPoint(m_CutPath[connection.item1].position), m_Mesh.transform.TransformPoint(vertices[connection.item2].position), 5f); From b8d9edff7c26579cf2108d84be5997b09fc407b4 Mon Sep 17 00:00:00 2001 From: Sachin Date: Mon, 25 May 2026 21:26:15 +0530 Subject: [PATCH 04/13] improve cut --- Editor/EditorCore/CutTool.cs | 43 ++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/Editor/EditorCore/CutTool.cs b/Editor/EditorCore/CutTool.cs index 247d870ce..934136026 100644 --- a/Editor/EditorCore/CutTool.cs +++ b/Editor/EditorCore/CutTool.cs @@ -220,7 +220,7 @@ public override void OnActivated() public override void OnWillBeDeactivated() { - if(!m_RectangleMode) + if(!m_RectangleMode && m_TargetFace != null && m_CutPath.Count > 1) ExecuteCut(false); Undo.undoRedoPerformed -= UndoRedoPerformed; @@ -233,20 +233,44 @@ public override void OnWillBeDeactivated() /// void Clear() { - m_Mesh = null; + ResetToolState(false); + } + + void ResetToolState(bool keepTarget) + { + if (!keepTarget) + m_Mesh = null; + m_TargetFace = null; m_CurrentFace = null; m_PlacingPoint = false; + m_SnappingPoint = false; + m_ModifyingPoint = false; m_CurrentCutCursor = null; + m_CurrentPosition = Vector3.positiveInfinity; + m_CurrentPositionNormal = Vector3.up; + m_CurrentVertexTypes = VertexTypes.None; m_CutPath.Clear(); m_MeshConnections.Clear(); + m_SnapedVertexId = -1; + m_SnapedEdge = Edge.Empty; + m_SelectedIndex = -2; + m_Dirty = false; m_RectDragging = false; m_RectStartPoint = Vector3.positiveInfinity; m_RectEndPoint = Vector3.positiveInfinity; - m_SelectedVertices = null; - m_SelectedEdges = null; + if (keepTarget && m_Mesh != null) + { + m_SelectedVertices = m_Mesh.sharedVertexLookup.Keys.ToArray(); + m_SelectedEdges = m_Mesh.faces.SelectMany(f => f.edges).Distinct().ToArray(); + } + else + { + m_SelectedVertices = null; + m_SelectedEdges = null; + } EditorHandleDrawing.ClearHandles(); @@ -470,8 +494,11 @@ void OnOverlayGUI(UObject target, SceneView view) } else { - if(GUILayout.Button(EditorGUIUtility.TrTextContent("Complete"))) - ExecuteCut(); + if(!m_RectangleMode && m_CutPath.Count > 1) + { + if(GUILayout.Button(EditorGUIUtility.TrTextContent("Complete"))) + ExecuteCut(); + } if(GUILayout.Button(EditorGUIUtility.TrTextContent("Cancel"))) ExitTool(); @@ -761,8 +788,6 @@ void DoRectanglePlacement(EditorWindow window) ActionResult result = DoCut(); EditorUtility.ShowNotification(result.notification); } - - Clear(); } m_RectStartPoint = Vector3.positiveInfinity; @@ -1149,7 +1174,7 @@ internal ActionResult DoCut() m_Mesh.SetSelectedFaces(newFaces); ProBuilderEditor.Refresh(); - Clear(); + ResetToolState(true); return new ActionResult(ActionResult.Status.Success, L10n.Tr("Cut executed")); } From 2bbf078c46bd5651f297c0f45f34e202c1741110 Mon Sep 17 00:00:00 2001 From: Sachin Date: Mon, 25 May 2026 22:06:03 +0530 Subject: [PATCH 05/13] fix cut tool --- Editor/EditorCore/CutTool.cs | 42 ++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/Editor/EditorCore/CutTool.cs b/Editor/EditorCore/CutTool.cs index 934136026..72e35b7d5 100644 --- a/Editor/EditorCore/CutTool.cs +++ b/Editor/EditorCore/CutTool.cs @@ -450,6 +450,9 @@ void OnOverlayGUI(UObject target, SceneView view) m_SnapToGrid = DoOverlayToggle(L10n.Tr("Snap to Grid"), m_SnapToGrid); EditorPrefs.SetBool(k_SnapToGridPrefKey, m_SnapToGrid); + m_SnapToGeometry = DoOverlayToggle(L10n.Tr("Snap to existing edges and vertices"), m_SnapToGeometry); + EditorPrefs.SetBool(k_SnapToGeometryPrefKey, m_SnapToGeometry); + if(m_RectangleMode) { EditorGUI.indentLevel++; @@ -459,12 +462,6 @@ void OnOverlayGUI(UObject target, SceneView view) } EditorGUI.indentLevel--; } - else - { - m_SnapToGeometry = DoOverlayToggle(L10n.Tr("Snap to existing edges and vertices"), m_SnapToGeometry); - EditorPrefs.SetBool(k_SnapToGeometryPrefKey, m_SnapToGeometry); - - } if(!m_RectangleMode && !m_SnapToGeometry) GUI.enabled = false; @@ -557,14 +554,14 @@ void DoPointPlacement(EditorWindow window) Event evt = Event.current; EventType evtType = evt.type; + m_SnappingPoint = m_SnapToGeometry || (evt.modifiers & EventModifiers.Control) != 0; + m_ModifyingPoint = evt.shift; + bool hasHitPosition = UpdateHitPosition(); //Updating visual helpers to get the right position and color to help in the placement if (evtType == EventType.Repaint) { - m_SnappingPoint = m_SnapToGeometry || (evt.modifiers & EventModifiers.Control) != 0; - m_ModifyingPoint = evt.shift; - if(!m_SnappingPoint && !m_ModifyingPoint && !m_PlacingPoint) @@ -655,14 +652,14 @@ void DoRectanglePlacement(EditorWindow window) Event evt = Event.current; EventType evtType = evt.type; + m_SnappingPoint = m_SnapToGeometry || (evt.modifiers & EventModifiers.Control) != 0; + m_ModifyingPoint = false; + bool hasHitPosition = UpdateHitPosition(); // Visual helpers if (evtType == EventType.Repaint) { - m_SnappingPoint = m_SnapToGeometry || (evt.modifiers & EventModifiers.Control) != 0; - m_ModifyingPoint = false; - if (hasHitPosition && IsCursorInSceneView(window)) { m_CurrentCutCursor = m_CutCursorTexture; @@ -738,7 +735,7 @@ void DoRectanglePlacement(EditorWindow window) Vector3 corner3 = start + faceUp * upDot; // Snap all corners to grid if enabled - if (m_SnapToGrid && EditorSnapSettings.gridSnapActive) + if (m_SnapToGrid) { corner0 = ProBuilderSnapping.Snap(corner0, EditorSnapping.activeMoveSnapValue); corner1 = ProBuilderSnapping.Snap(corner1, EditorSnapping.activeMoveSnapValue); @@ -938,7 +935,7 @@ internal void UpdateCurrentPosition(Face face, Vector3 position, Vector3 normal) /// void ApplyGridSnap() { - if (m_SnapToGrid && EditorSnapSettings.gridSnapActive) + if (m_SnapToGrid) m_CurrentPosition = ProBuilderSnapping.Snap(m_CurrentPosition, EditorSnapping.activeMoveSnapValue); } @@ -1136,7 +1133,7 @@ internal ActionResult DoCut() if(f == null) return new ActionResult(ActionResult.Status.Failure, L10n.Tr("Cut Shape is not valid")); - f.submeshIndex = m_TargetFace.submeshIndex; + ApplySourceFaceSettings(f, m_TargetFace); Vector3 nrm = Math.Normal(m_Mesh, f); Vector3 targetNrm = Math.Normal(m_Mesh, m_TargetFace); @@ -1245,7 +1242,7 @@ List ComputeNewFaces(Face face, IList cutVertexIndexes) Face newFace = ComputeFaceClosure(polygon, index, cutVertexSharedIndexes, out toDelete); if (newFace != null && newFace.indexesInternal != null) { - newFace.submeshIndex = m_TargetFace.submeshIndex; + ApplySourceFaceSettings(newFace, m_TargetFace); newFaces.Add(newFace); facesToDelete.AddRange(toDelete); } @@ -1392,6 +1389,19 @@ Face ComputeFaceClosure( List polygonStart, int currentIndex, List cut return bestFace; } + void ApplySourceFaceSettings(Face destination, Face source) + { + if (destination == null || source == null) + return; + + destination.submeshIndex = source.submeshIndex; + destination.manualUV = source.manualUV; + destination.uv = new AutoUnwrapSettings(source.uv); + destination.textureGroup = source.textureGroup; + destination.smoothingGroup = source.smoothingGroup; + destination.elementGroup = source.elementGroup; + } + bool TryGetFaceArea(Face face, List meshVertices, IList uniqueIdToVertexIndex, Dictionary sharedToUnique, out float area) { From 9919da72d295caed27c8b042f0930e1f5a1d0dde Mon Sep 17 00:00:00 2001 From: Sachin Date: Mon, 25 May 2026 22:33:43 +0530 Subject: [PATCH 06/13] modularize cut tool --- Editor/EditorCore/CutTool.Geometry.cs | 460 ++++++ Editor/EditorCore/CutTool.Geometry.cs.meta | 2 + Editor/EditorCore/CutTool.Rectangle.cs | 241 +++ Editor/EditorCore/CutTool.Rectangle.cs.meta | 2 + Editor/EditorCore/CutTool.Snapping.cs | 372 +++++ Editor/EditorCore/CutTool.Snapping.cs.meta | 2 + Editor/EditorCore/CutTool.UI.cs | 242 ++++ Editor/EditorCore/CutTool.UI.cs.meta | 2 + Editor/EditorCore/CutTool.Validation.cs | 134 ++ Editor/EditorCore/CutTool.Validation.cs.meta | 2 + Editor/EditorCore/CutTool.cs | 1369 +----------------- 11 files changed, 1461 insertions(+), 1367 deletions(-) create mode 100644 Editor/EditorCore/CutTool.Geometry.cs create mode 100644 Editor/EditorCore/CutTool.Geometry.cs.meta create mode 100644 Editor/EditorCore/CutTool.Rectangle.cs create mode 100644 Editor/EditorCore/CutTool.Rectangle.cs.meta create mode 100644 Editor/EditorCore/CutTool.Snapping.cs create mode 100644 Editor/EditorCore/CutTool.Snapping.cs.meta create mode 100644 Editor/EditorCore/CutTool.UI.cs create mode 100644 Editor/EditorCore/CutTool.UI.cs.meta create mode 100644 Editor/EditorCore/CutTool.Validation.cs create mode 100644 Editor/EditorCore/CutTool.Validation.cs.meta diff --git a/Editor/EditorCore/CutTool.Geometry.cs b/Editor/EditorCore/CutTool.Geometry.cs new file mode 100644 index 000000000..2b09e03b6 --- /dev/null +++ b/Editor/EditorCore/CutTool.Geometry.cs @@ -0,0 +1,460 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor.EditorTools; +using UnityEngine; +using UnityEngine.ProBuilder; +using UnityEngine.ProBuilder.MeshOperations; +using Vertex = UnityEngine.ProBuilder.Vertex; +using Edge = UnityEngine.ProBuilder.Edge; +using Math = UnityEngine.ProBuilder.Math; + +namespace UnityEditor.ProBuilder +{ + partial class CutTool + { + /// + /// Compute the cut result and display a notification + /// + void ExecuteCut(bool restorePrevious = true) + { + ActionResult result = DoCut(); + EditorUtility.ShowNotification(result.notification); + + if(restorePrevious) + ExitTool(); + } + + /// + /// Compute the faces resulting from the cut: + /// - First inserts points defining the cut as vertices in the face + /// - Compute the central polygon is the cut is creating a closed polygon in the face + /// - Update the rest of the face accordingly to the cut and the central polygon + /// + /// ActionResult success if it was possible to create the cut + internal ActionResult DoCut() + { + if (m_TargetFace == null || m_CutPath.Count < 2) + { + return new ActionResult(ActionResult.Status.Canceled, L10n.Tr("Not enough elements selected for a cut")); + } + + if(!m_IsCutValid) + { + return new ActionResult(ActionResult.Status.Failure, L10n.Tr("The current cut overlaps itself")); + } + + UndoUtility.RecordObject(m_Mesh, "Execute Cut"); + + List meshVertices = new List(); + m_Mesh.GetVerticesInList(meshVertices); + Vertex[] formerVertices = new Vertex[m_MeshConnections.Count]; + for(int i = 0; i < m_MeshConnections.Count; i++) + { + formerVertices[i] = meshVertices[m_MeshConnections[i].item2]; + } + + //Insert cut vertices in the mesh + List cutVertices = InsertVertices(); + m_Mesh.GetVerticesInList(meshVertices); + //Retrieve indexes of the cut points in the mesh vertices + int[] cutIndexes = cutVertices.Select(vert => meshVertices.IndexOf(vert)).ToArray(); + + //Update mesh connections with new indexes + for(int i = 0; i connection = m_MeshConnections[i]; + connection.item1 = meshVertices.IndexOf(cutVertices[connection.item1]); + connection.item2 = meshVertices.IndexOf(formerVertices[i]); + m_MeshConnections[i] = connection; + } + + List newFaces = new List(); + // If the cut defines a loop in the face, create the polygon corresponding to that loop + if (isALoop) + { + Face f = m_Mesh.CreatePolygon(cutIndexes, false); + + if(f == null) + return new ActionResult(ActionResult.Status.Failure, L10n.Tr("Cut Shape is not valid")); + + ApplySourceFaceSettings(f, m_TargetFace); + + Vector3 nrm = Math.Normal(m_Mesh, f); + Vector3 targetNrm = Math.Normal(m_Mesh, m_TargetFace); + // If the shape is define in the wrong orientation compared to the former face, reverse it + if(Vector3.Dot(nrm,targetNrm) < 0f) + f.Reverse(); + + newFaces.Add(f); + } + + //Compute the rest of the new faces (faces outside of the loop or division of the original face) + List faces = ComputeNewFaces(m_TargetFace, cutIndexes); + if(!isALoop) + newFaces.AddRange(faces); + + //Remove inserted vertices only if they were inserted for the process + List verticesIndexesToDelete = new List(); + for(int i = 0; i < m_CutPath.Count; i++) + { + if(( m_CutPath[i].types & VertexTypes.NewVertex ) != 0 + && ( m_CutPath[i].types & VertexTypes.VertexInShape ) == 0) + verticesIndexesToDelete.Add(cutIndexes[i]); + } + m_Mesh.DeleteVertices(verticesIndexesToDelete); + + //Delete former face + m_Mesh.DeleteFace(m_TargetFace); + + m_Mesh.ToMesh(); + m_Mesh.Refresh(); + m_Mesh.Optimize(); + + //Update mesh selection after the cut has been performed + MeshSelection.ClearElementSelection(); + m_Mesh.SetSelectedFaces(newFaces); + ProBuilderEditor.Refresh(); + + ResetToolState(true); + + return new ActionResult(ActionResult.Status.Success, L10n.Tr("Cut executed")); + } + + /// + /// Based on the new vertices inserted in the face, this method computes the different faces + /// created between the cut and the original face (external to the cut if it makes a loop) + /// + /// The faces are created by parsing the edges that defines the border of the original face. Is an edge ends on a + /// vertex that is part of the cut, or belongs to a connection between the cut and the face, + /// we close the defined polygon using the cut (though ComputeFaceClosure method) and create a face out of this polygon + /// + /// Original face to modify + /// Indexes of the new vertices inserted in the face + /// The list of polygons to create (defined by their vertices indexes) + List ComputeNewFaces(Face face, IList cutVertexIndexes) + { + List newFaces = new List(); + + //Get Vertices from the mesh + Dictionary sharedToUnique = m_Mesh.sharedVertexLookup; + var cutVertexSharedIndexes = cutVertexIndexes.Select(ind => sharedToUnique[ind]).ToList(); + + //Parse peripheral edges to unique id and find a common point between the peripheral edges and the cut + var peripheralEdges = WingedEdge.SortEdgesByAdjacency(face); + var peripheralEdgesUnique = new List(); + int startIndex = -1; + for (int i = 0; i < peripheralEdges.Count; i++) + { + Edge eShared = peripheralEdges[i]; + Edge eUnique = new Edge(sharedToUnique[eShared.a], sharedToUnique[eShared.b]); + peripheralEdgesUnique.Add(eUnique); + + if (startIndex == -1 && ( cutVertexSharedIndexes.Contains(eUnique.a) + || m_MeshConnections.Exists(tup => sharedToUnique[tup.item2] == eUnique.a))) + startIndex = i; + } + + //Create a polygon for each cut reaching the mesh edges + List facesToDelete = new List(); + List polygon = new List(); + for (int i = startIndex; i <= peripheralEdgesUnique.Count + startIndex; i++) + { + polygon.Add(peripheralEdges[i % peripheralEdgesUnique.Count].a); + Edge e = peripheralEdgesUnique[i % peripheralEdgesUnique.Count]; + + if(polygon.Count > 1) + { + int index = -1; + if(cutVertexSharedIndexes.Contains(e.a)) // get next vertex + { + index = e.a; + } + else if(m_MeshConnections.Exists(tup => sharedToUnique[tup.item2] == e.a)) + { + SimpleTuple connection = m_MeshConnections.Find(tup => sharedToUnique[tup.item2] == e.a); + polygon.Add(connection.item1); + index = sharedToUnique[connection.item1]; + } + + if(index >= 0) + { + // In the case of only 2 distinct, a face should not be added. + if (polygon.Count != 2) + { + List toDelete; + Face newFace = ComputeFaceClosure(polygon, index, cutVertexSharedIndexes, out toDelete); + if (newFace != null && newFace.indexesInternal != null) + { + ApplySourceFaceSettings(newFace, m_TargetFace); + newFaces.Add(newFace); + facesToDelete.AddRange(toDelete); + } + } + + //Start a new polygon + polygon = new List(); + polygon.Add(peripheralEdges[i % peripheralEdgesUnique.Count].a); + } + } + } + polygon.Clear(); + + m_Mesh.DeleteFaces(facesToDelete); + return newFaces; + } + + /// + /// The method computes all the possible faces that can be made starting by the vertices in polygonStart and ending with the cut + /// This method creates faces that are not the final one and that must be deleted at the end. These invalid faces are returned in facesToDelete + /// The only valid face is returned from this method. From all defined faces, the valid face is the one with the smaller area + /// (otherwise it means it covers another face of the mesh). + /// + /// Indexes of the first vertices of the new Face to define, these vertices are coming from the original face only + /// Current vertex index in the cut + /// Indexes of the vertices defining the cut + /// out : extra faces created by this method that will need to be deleted after + /// (these faces cannot be deleted directly as it will break the m_MeshConnections by deleting some indexes before the end of the algorithm) + /// the valid face that need to be kept in the resulting mesh + Face ComputeFaceClosure( List polygonStart, int currentIndex, List cutIndexes, out List facesToDelete) + { + List meshVertices = new List(); + IList uniqueIdToVertexIndex = m_Mesh.sharedVertices; + Dictionary sharedToUnique = m_Mesh.sharedVertexLookup; + + facesToDelete = new List(); + + if (polygonStart == null || polygonStart.Count == 0 || cutIndexes == null || cutIndexes.Count == 0) + return null; + + int polygonFirstVertex = polygonStart[0]; + int startIndex = cutIndexes.IndexOf(currentIndex); + + if (startIndex < 0 || !sharedToUnique.ContainsKey(polygonFirstVertex)) + return null; + + int polygonFirstSharedIndex = sharedToUnique[polygonFirstVertex]; + + bool hasConnection = m_MeshConnections.Exists(tup => sharedToUnique.ContainsKey(tup.item2) + && sharedToUnique[tup.item2] == polygonFirstSharedIndex); + SimpleTuple connection = default; + + if (hasConnection) + connection = m_MeshConnections.Find(tup => sharedToUnique.ContainsKey(tup.item2) + && sharedToUnique[tup.item2] == polygonFirstSharedIndex); + + List> closureCandidates = new List>(); + + //Go through the cut in reverse direction + int index; + int finalIndex = isALoop ?(startIndex - cutIndexes.Count) : 0; + bool connected = false; + List candidate = new List(); + for(index = startIndex - 1; index >= finalIndex; index--) + { + int vertexIndex = uniqueIdToVertexIndex[cutIndexes[(index + cutIndexes.Count) % cutIndexes.Count]][0]; + candidate.Add(vertexIndex); + if(sharedToUnique[vertexIndex] == polygonFirstSharedIndex || + (hasConnection && sharedToUnique.ContainsKey(connection.item1) + && sharedToUnique[vertexIndex] == sharedToUnique[connection.item1])) + { + connected = true; + break; + } + } + + //If we find a valid candidate for the connection, add it to the list + if(connected) + closureCandidates.Add(candidate); + + //Go through the cut in forward direction + finalIndex = isALoop ? (startIndex + cutIndexes.Count) : cutIndexes.Count; + connected = false; + candidate = new List(); + for(index = startIndex + 1; index < finalIndex; index++) + { + int vertexIndex = uniqueIdToVertexIndex[cutIndexes[index % cutIndexes.Count]][0]; + candidate.Add(vertexIndex); + if(sharedToUnique[vertexIndex] == polygonFirstSharedIndex || + (hasConnection && sharedToUnique.ContainsKey(connection.item1) + && sharedToUnique[vertexIndex] == sharedToUnique[connection.item1])) + { + connected = true; + break; + } + } + + //If we find a valid candidate for the connection, add it to the list + if(connected) + closureCandidates.Add(candidate); + + //Go through the different candidate and keep the best one. + Face bestFace = null; + float bestArea = 0f; + foreach(var closure in closureCandidates) + { + closure.AddRange(polygonStart); + + Face face = m_Mesh.CreatePolygon(closure, false); + meshVertices.Clear(); + m_Mesh.GetVerticesInList(meshVertices); + uniqueIdToVertexIndex = m_Mesh.sharedVertices; + sharedToUnique = m_Mesh.sharedVertexLookup; + + float area; + if (!TryGetFaceArea(face, meshVertices, uniqueIdToVertexIndex, sharedToUnique, out area)) + { + if (face != null) + facesToDelete.Add(face); + + continue; + } + + if(bestFace != null) + { + if(area < bestArea) + { + facesToDelete.Add(bestFace); + bestArea = area; + bestFace = face; + } + else + facesToDelete.Add(face); + } + else + { + bestFace = face; + bestArea = area; + } + } + + return bestFace; + } + + void ApplySourceFaceSettings(Face destination, Face source) + { + if (destination == null || source == null) + return; + + destination.submeshIndex = source.submeshIndex; + destination.manualUV = source.manualUV; + destination.uv = new AutoUnwrapSettings(source.uv); + destination.textureGroup = source.textureGroup; + destination.smoothingGroup = source.smoothingGroup; + destination.elementGroup = source.elementGroup; + } + + bool TryGetFaceArea(Face face, List meshVertices, IList uniqueIdToVertexIndex, + Dictionary sharedToUnique, out float area) + { + area = 0f; + + if (face == null || face.indexesInternal == null) + return false; + + Vector3[] vertices = meshVertices.Select(vertex => vertex.position).ToArray(); + int[] indexes = new int[face.indexesInternal.Length]; + + for (int i = 0; i < face.indexesInternal.Length; i++) + { + int uniqueIndex; + if (!sharedToUnique.TryGetValue(face.indexesInternal[i], out uniqueIndex)) + return false; + + if (uniqueIndex < 0 || uniqueIndex >= uniqueIdToVertexIndex.Count) + return false; + + SharedVertex sharedVertex = uniqueIdToVertexIndex[uniqueIndex]; + if (sharedVertex == null || sharedVertex.Count < 1) + return false; + + indexes[i] = sharedVertex[0]; + } + + area = Math.PolygonArea(vertices, indexes); + return true; + } + + /// + /// Insert all position from the cut path to the current faces as new vertices + /// + /// The list of Vertex inserted in the face + List InsertVertices() + { + List newVertices = new List(); + + foreach (var vertexData in m_CutPath) + { + switch (vertexData.types) + { + case VertexTypes.ExistingVertex: + case VertexTypes.VertexInShape: + newVertices.Add(InsertVertexOnExistingVertex(vertexData.position)); + break; + case VertexTypes.AddedOnEdge: + newVertices.Add(InsertVertexOnExistingEdge(vertexData.position)); + break; + case VertexTypes.NewVertex: + newVertices.Add(m_Mesh.InsertVertexInMesh(vertexData.position,vertexData.normal)); + break; + default: + break; + } + } + + return newVertices; + } + + /// + /// Method to retrieve a vertex already existing in the face to avoid duplicated + /// + /// The vertex position + /// The retrieved vertex + Vertex InsertVertexOnExistingVertex(Vector3 vertexPosition) + { + Vertex vertex = null; + + List vertices = m_Mesh.GetVertices().ToList(); + for (int vertIndex = 0; vertIndex < vertices.Count; vertIndex++) + { + if (Math.Approx3(vertices[vertIndex].position, vertexPosition) + && !float.IsNaN(vertices[vertIndex].normal.x) ) + { + vertex = vertices[vertIndex]; + break; + } + } + + return vertex; + } + + /// + /// Insert the vertex in an exiting edge + /// + /// The position of the vertex to insert + /// The inew vertex inserted + Vertex InsertVertexOnExistingEdge(Vector3 vertexPosition) + { + List vertices = m_Mesh.GetVertices().ToList(); + List peripheralEdges = WingedEdge.SortEdgesByAdjacency(m_TargetFace); + + int bestIndex = -1; + float bestDistance = Mathf.Infinity; + for (int i = 0; i < peripheralEdges.Count; i++) + { + float dist = UnityEngine.ProBuilder.Math.DistancePointLineSegment(vertexPosition, + vertices[peripheralEdges[i].a].position, + vertices[peripheralEdges[i].b].position); + + if (dist < bestDistance) + { + bestIndex = i; + bestDistance = dist; + } + } + + Vertex v = m_Mesh.InsertVertexOnEdge(peripheralEdges[bestIndex], vertexPosition); + return v; + } + } +} diff --git a/Editor/EditorCore/CutTool.Geometry.cs.meta b/Editor/EditorCore/CutTool.Geometry.cs.meta new file mode 100644 index 000000000..3c0880ee8 --- /dev/null +++ b/Editor/EditorCore/CutTool.Geometry.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 592402103997a544d830313970ae86f0 \ No newline at end of file diff --git a/Editor/EditorCore/CutTool.Rectangle.cs b/Editor/EditorCore/CutTool.Rectangle.cs new file mode 100644 index 000000000..339eb5edf --- /dev/null +++ b/Editor/EditorCore/CutTool.Rectangle.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor.EditorTools; +using UnityEngine; +using UnityEngine.ProBuilder; +using UnityEngine.ProBuilder.MeshOperations; +using UnityEngine.UIElements; +using Cursor = UnityEngine.Cursor; +using Edge = UnityEngine.ProBuilder.Edge; +using Math = UnityEngine.ProBuilder.Math; +using UObject = UnityEngine.Object; +using RaycastHit = UnityEngine.ProBuilder.RaycastHit; +using UHandleUtility = UnityEditor.HandleUtility; + +using ToolManager = UnityEditor.EditorTools.ToolManager; +using Vertex = UnityEngine.ProBuilder.Vertex; + +namespace UnityEditor.ProBuilder +{ + partial class CutTool + { + /// + /// Rectangle mode: click and drag to define a rectangular cut on the face. + /// On mouse up, auto-places the 4 corners and executes the cut. + /// + void DoRectanglePlacement(EditorWindow window) + { + Event evt = Event.current; + EventType evtType = evt.type; + + m_SnappingPoint = m_SnapToGeometry || (evt.modifiers & EventModifiers.Control) != 0; + m_ModifyingPoint = false; + + bool hasHitPosition = UpdateHitPosition(); + + // Visual helpers + if (evtType == EventType.Repaint) + { + if (hasHitPosition && IsCursorInSceneView(window)) + { + m_CurrentCutCursor = m_CutCursorTexture; + m_CurrentHandleColor = k_HandleColorAddNewVertex; + } + else + { + m_CurrentCutCursor = null; + m_CurrentPosition = Vector3.positiveInfinity; + } + } + + // Mouse down: start rectangle drag + if (hasHitPosition + && evtType == EventType.MouseDown && evt.button == 0 + && HandleUtility.nearestControl == m_ControlId + && !m_RectDragging) + { + m_RectDragging = true; + m_RectStartPoint = m_CurrentPosition; + m_RectEndPoint = m_CurrentPosition; + m_TargetFace = m_CurrentFace; + + var edges = m_TargetFace.edges; + m_SelectedVertices = edges.Select(e => e.a).ToArray(); + m_SelectedEdges = edges.ToArray(); + + m_CutPath.Clear(); + m_MeshConnections.Clear(); + evt.Use(); + } + + // Mouse drag: update rectangle end point + if (m_RectDragging && evtType == EventType.MouseDrag && evt.button == 0) + { + if (hasHitPosition && m_CurrentFace == m_TargetFace) + { + m_RectEndPoint = m_CurrentPosition; + } + evt.Use(); + } + + // Mouse up: finalize the rectangle and execute cut + if (m_RectDragging + && (evtType == EventType.MouseUp && evt.button == 0)) + { + m_RectDragging = false; + + // Project start/end onto the face plane to compute the other 2 corners + Vector3 start = m_RectStartPoint; + Vector3 end = m_RectEndPoint; + + // Compute face normal for projection + Vector3 faceNormal = Math.Normal(m_Mesh, m_TargetFace); + + // Create a local coordinate system on the face plane + Vector3 faceRight, faceUp; + if (Mathf.Abs(Vector3.Dot(faceNormal, Vector3.up)) > 0.99f) + faceRight = Vector3.Cross(faceNormal, Vector3.forward).normalized; + else + faceRight = Vector3.Cross(faceNormal, Vector3.up).normalized; + faceUp = Vector3.Cross(faceNormal, faceRight).normalized; + + // Decompose rect diagonals in face space + Vector3 diagonal = end - start; + float rightDot = Vector3.Dot(diagonal, faceRight); + float upDot = Vector3.Dot(diagonal, faceUp); + + // Compute the 4 rectangle corners in world space + Vector3 corner0 = start; + Vector3 corner1 = start + faceRight * rightDot; + Vector3 corner2 = end; + Vector3 corner3 = start + faceUp * upDot; + + // Snap all corners to grid if enabled + if (m_SnapToGrid) + { + corner0 = ProBuilderSnapping.Snap(corner0, EditorSnapping.activeMoveSnapValue); + corner1 = ProBuilderSnapping.Snap(corner1, EditorSnapping.activeMoveSnapValue); + corner2 = ProBuilderSnapping.Snap(corner2, EditorSnapping.activeMoveSnapValue); + corner3 = ProBuilderSnapping.Snap(corner3, EditorSnapping.activeMoveSnapValue); + } + + if (HasSignificantRectangle(corner0, corner2)) + { + // Build cut path: 4 corners + close back to start to form a loop + UndoUtility.RecordObject(this, "Rectangle Cut"); + + m_CurrentPosition = corner0; + m_CurrentPositionNormal = faceNormal; + m_CurrentVertexTypes = VertexTypes.NewVertex; + m_CurrentFace = m_TargetFace; + AddCurrentPositionToPath(false); + + m_CurrentPosition = corner1; + m_CurrentPositionNormal = faceNormal; + m_CurrentVertexTypes = VertexTypes.NewVertex; + AddCurrentPositionToPath(false); + + m_CurrentPosition = corner2; + m_CurrentPositionNormal = faceNormal; + m_CurrentVertexTypes = VertexTypes.NewVertex; + AddCurrentPositionToPath(false); + + m_CurrentPosition = corner3; + m_CurrentPositionNormal = faceNormal; + m_CurrentVertexTypes = VertexTypes.NewVertex; + AddCurrentPositionToPath(false); + + // Close the loop by returning to the start corner + m_CurrentPosition = corner0; + m_CurrentPositionNormal = faceNormal; + m_CurrentVertexTypes = VertexTypes.VertexInShape; + m_CurrentFace = m_TargetFace; + AddCurrentPositionToPath(false); + + // RebuildCutShape(true) already executes the cut if the path is valid + // Don't call DoCut() again — it would run on already-cleared data + RebuildCutShape(false); + + if (m_CutPath.Count >= 2 && m_IsCutValid) + { + ActionResult result = DoCut(); + EditorUtility.ShowNotification(result.notification); + } + } + + m_RectStartPoint = Vector3.positiveInfinity; + m_RectEndPoint = Vector3.positiveInfinity; + evt.Use(); + } + + if (TryPassThroughSelection(window, hasHitPosition)) + return; + } + + bool HasSignificantRectangle(Vector3 start, Vector3 end) + { + return Vector3.Distance(start, end) > 0.001f; + } + + /// + /// Draw the rectangle preview during a drag operation. + /// + void DoRectanglePreview() + { + if (!m_RectDragging || m_Mesh == null || m_TargetFace == null) + return; + + Transform trs = m_Mesh.transform; + Vector3 startW = trs.TransformPoint(m_RectStartPoint); + Vector3 endW = trs.TransformPoint(m_RectEndPoint); + + // Compute face plane + Vector3 faceNormal = Math.Normal(m_Mesh, m_TargetFace); + + Vector3 faceRight, faceUp; + if (Mathf.Abs(Vector3.Dot(faceNormal, Vector3.up)) > 0.99f) + faceRight = Vector3.Cross(faceNormal, Vector3.forward).normalized; + else + faceRight = Vector3.Cross(faceNormal, Vector3.up).normalized; + faceUp = Vector3.Cross(faceNormal, faceRight).normalized; + + Vector3 diagonal = endW - startW; + float rightDot = Vector3.Dot(diagonal, faceRight); + float upDot = Vector3.Dot(diagonal, faceUp); + + Vector3 c0 = startW; + Vector3 c1 = startW + faceRight * rightDot; + Vector3 c2 = endW; + Vector3 c3 = startW + faceUp * upDot; + + // Draw filled rectangle + Handles.color = k_RectPreviewColor; + Handles.DrawAAConvexPolygon(new Vector3[] { c0, c1, c2, c3 }); + + // Draw outline + Handles.color = k_RectOutlineColor; + Handles.DrawAAPolyLine(2f, new Vector3[] { c0, c1, c2, c3, c0 }); + } + + bool TryPassThroughSelection(EditorWindow window, bool hasHitPosition) + { + if (m_CutPath.Count != 0 + || hasHitPosition + || HandleUtility.nearestControl != m_ControlId) + { + return false; + } + + SceneView sceneView = window as SceneView; + if (sceneView == null) + sceneView = SceneView.lastActiveSceneView; + + if (sceneView == null || ProBuilderEditor.instance == null) + return false; + + ProBuilderEditor.instance.HandleMouseEvent(sceneView, m_ControlId); + return true; + } + } +} \ No newline at end of file diff --git a/Editor/EditorCore/CutTool.Rectangle.cs.meta b/Editor/EditorCore/CutTool.Rectangle.cs.meta new file mode 100644 index 000000000..78e0e2ea3 --- /dev/null +++ b/Editor/EditorCore/CutTool.Rectangle.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 54561049d8d6e5b4488fae269a4a9969 \ No newline at end of file diff --git a/Editor/EditorCore/CutTool.Snapping.cs b/Editor/EditorCore/CutTool.Snapping.cs new file mode 100644 index 000000000..9664fd18c --- /dev/null +++ b/Editor/EditorCore/CutTool.Snapping.cs @@ -0,0 +1,372 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.ProBuilder; +using Edge = UnityEngine.ProBuilder.Edge; +using Math = UnityEngine.ProBuilder.Math; +using RaycastHit = UnityEngine.ProBuilder.RaycastHit; +using UHandleUtility = UnityEditor.HandleUtility; + +namespace UnityEditor.ProBuilder +{ + partial class CutTool + { + /// + /// Compute the position designated by the user in the current mesh/face taking into account snapping + /// + /// true is a valid position is computed in the mesh + bool UpdateHitPosition() + { + Event evt = Event.current; + + Ray ray = UHandleUtility.GUIPointToWorldRay(evt.mousePosition); + RaycastHit pbHit; + + m_CurrentFace = null; + + if (UnityEngine.ProBuilder.HandleUtility.FaceRaycast(ray, m_Mesh, out pbHit)) + { + UpdateCurrentPosition(m_Mesh.faces[pbHit.face], pbHit.point ,pbHit.normal); + return true; + } + + return false; + } + + /// + /// Add the position in the face to the cut taking into account snapping + /// + internal void UpdateCurrentPosition(Face face, Vector3 position, Vector3 normal) + { + m_CurrentPosition = position; + m_CurrentPositionNormal = normal; + m_CurrentFace = face; + m_CurrentVertexTypes = VertexTypes.None; + + CheckPointInCutPath(); + + if (m_CurrentVertexTypes == VertexTypes.None && !m_ModifyingPoint) + CheckPointInMesh(); + + // Only apply grid snap if the point is a free-floating new vertex on the face + // (don't pull already-snapped edge/vertex points off their targets) + if (m_CurrentVertexTypes == VertexTypes.NewVertex || m_CurrentVertexTypes == VertexTypes.None) + ApplyGridSnap(); + } + + /// + /// Snap the current position to Unity's grid if grid snapping is enabled. + /// + void ApplyGridSnap() + { + if (m_SnapToGrid) + m_CurrentPosition = ProBuilderSnapping.Snap(m_CurrentPosition, EditorSnapping.activeMoveSnapValue); + } + + /// + /// Updates the connections between the cut path and the mesh vertices + /// + internal void UpdateMeshConnections() + { + m_MeshConnections.Clear(); + if(m_CutPath.Count < 2) + return; + + List existingVerticesInCut = + m_CutPath.Where(v => ( v.types | VertexTypes.ExistingVertex ) != 0) + .Select(v => v.position).ToList(); + + Vector3[] verticesPositions = m_Mesh.positionsInternal; + if(!isALoop) + { + //Connects to start and the end of the path to create a loop + float minDistToStart = Single.PositiveInfinity, minDistToStart2 = Single.PositiveInfinity; + float minDistToEnd = Single.PositiveInfinity, minDistToEnd2 = Single.PositiveInfinity; + int bestVertexIndexToStart = -1, bestVertexIndexToStart2 = -1, bestVertexIndexToEnd = -1, bestVertexIndexToEnd2 = -1; + float dist; + foreach(var vertexIndex in m_TargetFace.distinctIndexes) + { + if(existingVerticesInCut.Count > 0) + { + if(existingVerticesInCut.Exists(vert => Math.Approx3(verticesPositions[vertexIndex], vert))) + continue; + } + + if(( m_CutPath[0].types & VertexTypes.NewVertex ) != 0) + { + dist = Vector3.Distance(verticesPositions[vertexIndex], m_CutPath[0].position); + if(dist < minDistToStart) + { + minDistToStart2 = minDistToStart; + bestVertexIndexToStart2 = bestVertexIndexToStart; + minDistToStart = dist; + bestVertexIndexToStart = vertexIndex; + }else if(dist < minDistToStart2) + { + minDistToStart2 = dist; + bestVertexIndexToStart2 = vertexIndex; + } + } + if(m_CutPath.Count > 1 && ( m_CutPath[m_CutPath.Count - 1].types & VertexTypes.NewVertex ) != 0) + { + dist = Vector3.Distance(verticesPositions[vertexIndex], m_CutPath[m_CutPath.Count - 1].position); + if(dist < minDistToEnd) + { + minDistToEnd2 = minDistToEnd; + bestVertexIndexToEnd2 = bestVertexIndexToEnd; + minDistToEnd = dist; + bestVertexIndexToEnd = vertexIndex; + } + else if(dist < minDistToEnd2) + { + minDistToEnd2 = dist; + bestVertexIndexToEnd2 = vertexIndex; + } + } + } + + //Do not connect the 2 extremities to the same point + if(bestVertexIndexToStart == bestVertexIndexToEnd) + { + if(minDistToStart2 < minDistToEnd2) + bestVertexIndexToStart = bestVertexIndexToStart2; + else + bestVertexIndexToEnd = bestVertexIndexToEnd2; + } + + if(bestVertexIndexToStart >= 0) + m_MeshConnections.Add(new SimpleTuple(0,bestVertexIndexToStart)); + + if(bestVertexIndexToEnd >= 0) + m_MeshConnections.Add(new SimpleTuple(m_CutPath.Count - 1,bestVertexIndexToEnd)); + } + else if(isALoop && connectionsToBordersCount < 2) + { + //The path must have minimum connections with the face borders, find the closest vertices + foreach(var vertexIndex in m_TargetFace.distinctIndexes) + { + if(existingVerticesInCut.Count > 0) + { + if(existingVerticesInCut.Exists(vert => Math.Approx3(verticesPositions[vertexIndex], vert))) + continue; + } + + int pathIndex = -1; + float minDistance = Single.MaxValue; + for(int i = 0; i < m_CutPath.Count; i++) + { + if(( m_CutPath[i].types & (VertexTypes.AddedOnEdge | VertexTypes.ExistingVertex) ) == 0) + { + float dist = Vector3.Distance(verticesPositions[vertexIndex], m_CutPath[i].position); + if(dist < minDistance) + { + minDistance = dist; + pathIndex = i; + } + } + } + + if(pathIndex >= 0) + { + if(m_MeshConnections.Exists(tup => tup.item1 == pathIndex)) + { + var tuple = m_MeshConnections.Find(tup => tup.item1 == pathIndex); + if(Vector3.Distance(m_CutPath[tuple.item1].position, verticesPositions[tuple.item2]) + > Vector3.Distance(m_CutPath[pathIndex].position, verticesPositions[vertexIndex])) + { + m_MeshConnections.Remove(tuple); + m_MeshConnections.Add(new SimpleTuple(pathIndex, vertexIndex)); + } + } + else + m_MeshConnections.Add(new SimpleTuple(pathIndex, vertexIndex)); + } + } + + m_MeshConnections.Sort((a,b) => + (int)Mathf.Sign(Vector3.Distance(m_CutPath[a.item1].position, verticesPositions[a.item2]) + - Vector3.Distance(m_CutPath[b.item1].position, verticesPositions[b.item2]))); + + int requiredConnections = m_RectangleMode ? 4 : 2; + int connectionsCount = Mathf.Max(0, requiredConnections - connectionsToBordersCount); + connectionsCount = Mathf.Min(connectionsCount, m_MeshConnections.Count); + m_MeshConnections.RemoveRange(connectionsCount,m_MeshConnections.Count - connectionsCount); + } + } + + /// + /// Check whether the current position (m_CurrentPosition) can be associated/snapped to an existing position of the path + /// + void CheckPointInCutPath() + { + //Check if trying to reach the start point + if(!m_ModifyingPoint && m_CutPath.Count > 1) + { + float snapDistance = 0.1f; + var vertexData = m_CutPath[0]; + if(Math.Approx3(vertexData.position, m_CurrentPosition, snapDistance)) + { + m_CurrentPosition = vertexData.position; + m_CurrentVertexTypes = vertexData.types | VertexTypes.VertexInShape; + m_SelectedIndex = 0; + } + } + else if (m_SnappingPoint || m_ModifyingPoint) + { + float snapDistance = m_SnappingDistance; + for(int i = 0; i < m_CutPath.Count; i++) + { + var vertexData = m_CutPath[i]; + if(Math.Approx3(vertexData.position, + m_CurrentPosition, + snapDistance)) + { + snapDistance = Vector3.Distance(vertexData.position, m_CurrentPosition); + if(!m_ModifyingPoint) + m_CurrentPosition = vertexData.position; + m_CurrentVertexTypes = vertexData.types | VertexTypes.VertexInShape; + m_SelectedIndex = i; + } + } + } + } + + /// + /// Check whether the current position (m_CurrentPosition) can be associated/snapped to an existing + /// edge or vertex of the current face + /// + void CheckPointInMesh() + { + m_CurrentVertexTypes = VertexTypes.NewVertex; + bool snapedOnVertex = false; + float snapDistance = m_SnappingDistance; + int bestIndex = -1; + float bestDistance = Mathf.Infinity; + + m_SnapedVertexId = -1; + m_SnapedEdge = Edge.Empty; + + Vertex[] vertices = m_Mesh.GetVertices(); + List peripheralEdges = WingedEdge.SortEdgesByAdjacency(m_CurrentFace); + if (m_TargetFace != null && m_CurrentFace != m_TargetFace) + peripheralEdges = WingedEdge.SortEdgesByAdjacency(m_TargetFace); + for (int i = 0; i < peripheralEdges.Count; i++) + { + if ((m_TargetFace == null || m_TargetFace == m_CurrentFace) && m_SnappingPoint) + { + if (Math.Approx3(vertices[peripheralEdges[i].a].position, + m_CurrentPosition, + snapDistance)) + { + bestIndex = i; + snapedOnVertex = true; + break; + } + else + { + float dist = Math.DistancePointLineSegment( + m_CurrentPosition, + vertices[peripheralEdges[i].a].position, + vertices[peripheralEdges[i].b].position); + + if (dist < Mathf.Min(snapDistance, bestDistance)) + { + bestIndex = i; + bestDistance = dist; + } + } + } + //Even with no snapping, try to detect if the first point is on a existing geometry + else if(m_TargetFace == null && !m_SnappingPoint) + { + if (Math.Approx3(vertices[peripheralEdges[i].a].position, + m_CurrentPosition, + 0.01f)) + { + bestIndex = i; + snapedOnVertex = true; + break; + } + else + { + float dist = Math.DistancePointLineSegment( + m_CurrentPosition, + vertices[peripheralEdges[i].a].position, + vertices[peripheralEdges[i].b].position); + + if (dist < Mathf.Min(0.01f, bestDistance)) + { + bestIndex = i; + bestDistance = dist; + } + } + } + else if(m_CurrentFace != m_TargetFace && m_TargetFace != null ) + { + float edgeDist = Math.DistancePointLineSegment(m_CurrentPosition, + vertices[peripheralEdges[i].a].position, + vertices[peripheralEdges[i].b].position); + + float vertexDist = Vector3.Distance(m_CurrentPosition, + vertices[peripheralEdges[i].a].position); + + if (edgeDist < vertexDist && edgeDist < bestDistance) + { + bestIndex = i; + bestDistance = edgeDist; + snapedOnVertex = false; + } + //always prioritize vertex snap on edge snap + else if (vertexDist <= bestDistance) + { + bestIndex = i; + bestDistance = vertexDist; + snapedOnVertex = true; + } + } + } + + //We found a close vertex + if (snapedOnVertex) + { + m_CurrentPosition = vertices[peripheralEdges[bestIndex].a].position; + m_CurrentVertexTypes = VertexTypes.ExistingVertex; + m_SelectedIndex = -1; + + m_SnapedVertexId = peripheralEdges[bestIndex].a; + CheckPointInCutPath(); + } + //If not, did we found a close edge? + else if (bestIndex >= 0) + { + if (m_TargetFace == null || m_TargetFace == m_CurrentFace) + { + Vector3 left = vertices[peripheralEdges[bestIndex].a].position, + right = vertices[peripheralEdges[bestIndex].b].position; + + float x = (m_CurrentPosition - left).magnitude; + float y = (m_CurrentPosition - right).magnitude; + + m_CurrentPosition = left + (x / (x + y)) * (right - left); + } + else //if(m_CurrentFace != m_TargetFace) + { + Vector3 a = m_CurrentPosition - + vertices[peripheralEdges[bestIndex].a].position; + Vector3 b = vertices[peripheralEdges[bestIndex].b].position - + vertices[peripheralEdges[bestIndex].a].position; + + float angle = Vector3.Angle(b, a); + m_CurrentPosition = Vector3.Magnitude(a) * Mathf.Cos(angle * Mathf.Deg2Rad) * b / Vector3.Magnitude(b); + m_CurrentPosition += vertices[peripheralEdges[bestIndex].a].position; + } + + m_SnapedEdge = peripheralEdges[bestIndex]; + + m_CurrentVertexTypes = VertexTypes.AddedOnEdge; + m_SelectedIndex = -1; + } + } + } +} diff --git a/Editor/EditorCore/CutTool.Snapping.cs.meta b/Editor/EditorCore/CutTool.Snapping.cs.meta new file mode 100644 index 000000000..7d237db30 --- /dev/null +++ b/Editor/EditorCore/CutTool.Snapping.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 48cea5a6844fe7342b623371786a86be \ No newline at end of file diff --git a/Editor/EditorCore/CutTool.UI.cs b/Editor/EditorCore/CutTool.UI.cs new file mode 100644 index 000000000..2b2053b0a --- /dev/null +++ b/Editor/EditorCore/CutTool.UI.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using UnityEngine.ProBuilder; +using Edge = UnityEngine.ProBuilder.Edge; +using UObject = UnityEngine.Object; + +namespace UnityEditor.ProBuilder +{ + partial class CutTool + { + /// + /// Overlay GUI + /// + /// the target of this overlay + /// the current SceneView where to display the overlay + void OnOverlayGUI(UObject target, SceneView view) + { + if(MeshSelection.selectedObjectCount != 1) + { + var rect = EditorGUILayout.GetControlRect(false, 45); + EditorGUI.HelpBox(rect, L10n.Tr("One and only one ProBuilder mesh must be selected."), MessageType.Warning); + } + + GUI.enabled = MeshSelection.selectedObjectCount == 1; + + m_RectangleMode = DoOverlayToggle(L10n.Tr("Rectangle Mode"), m_RectangleMode); + EditorPrefs.SetBool(k_RectangleModePrefKey, m_RectangleMode); + + m_SnapToGrid = DoOverlayToggle(L10n.Tr("Snap to Grid"), m_SnapToGrid); + EditorPrefs.SetBool(k_SnapToGridPrefKey, m_SnapToGrid); + + m_SnapToGeometry = DoOverlayToggle(L10n.Tr("Snap to existing edges and vertices"), m_SnapToGeometry); + EditorPrefs.SetBool(k_SnapToGeometryPrefKey, m_SnapToGeometry); + + if(m_RectangleMode) + { + EditorGUI.indentLevel++; + using(new GUILayout.HorizontalScope()) + { + EditorGUILayout.LabelField(L10n.Tr("Click & drag to draw a rectangle cut"), GUILayout.Width(250)); + } + EditorGUI.indentLevel--; + } + + if(!m_RectangleMode && !m_SnapToGeometry) + GUI.enabled = false; + EditorGUI.indentLevel++; + using(new GUILayout.HorizontalScope()) + { + EditorGUILayout.LabelField(L10n.Tr("Snapping distance"), GUILayout.Width(200)); + m_SnappingDistance = EditorGUILayout.FloatField(m_SnappingDistance); + EditorPrefs.SetFloat( k_SnappingDistancePrefKey, m_SnappingDistance); + } + EditorGUI.indentLevel--; + + GUI.enabled = true; + + if(MeshSelection.selectedObjectCount != 1) + GUI.enabled = false; + + using(new GUILayout.HorizontalScope()) + { + if(m_Mesh == null) + { + if(GUILayout.Button(EditorGUIUtility.TrTextContent("Start"))) + UpdateTarget(); + + if(GUILayout.Button(EditorGUIUtility.TrTextContent("Quit"))) + ExitTool(); + } + else + { + if(!m_RectangleMode && m_CutPath.Count > 1) + { + if(GUILayout.Button(EditorGUIUtility.TrTextContent("Complete"))) + ExecuteCut(); + } + + if(GUILayout.Button(EditorGUIUtility.TrTextContent("Cancel"))) + ExitTool(); + } + } + + GUI.enabled = true; + } + + /// + /// Creates a toggle for cut tool overlays. + /// + /// toggle title + /// starting value for the toggle + /// new toggle value + bool DoOverlayToggle(string label, bool val) + { + using(new GUILayout.HorizontalScope()) + { + EditorGUILayout.LabelField(label, GUILayout.Width(225)); + GUILayout.FlexibleSpace(); + return EditorGUILayout.Toggle(val); + } + } + + /// + /// Display existing points of the cut + /// + void DoExistingPointsGUI() + { + Transform trs = m_Mesh.transform; + int len = m_CutPath.Count; + + Event evt = Event.current; + + if (evt.type == EventType.Repaint) + { + for (int index = 0; index < len; index++) + { + Vector3 point = trs.TransformPoint(m_CutPath[index].position); + float size = HandleUtility.GetHandleSize(point) * k_HandleSize; + + Handles.color = k_HandleColor; + Handles.DotHandleCap(-1, point, Quaternion.identity, size, evt.type); + } + + Handles.color = Color.white; + } + } + + /// + /// Display current position to potentially add to the cut + /// + void DoCurrentPointsGUI() + { + Transform trs = m_Mesh.transform; + int len = m_CutPath.Count; + + Event evt = Event.current; + + if (evt.type == EventType.Repaint) + { + if (!m_CurrentPosition.Equals(Vector3.positiveInfinity)) + { + Vector3 point = trs.TransformPoint(m_CurrentPosition); + if(m_SelectedIndex >= 0 && m_SelectedIndex < m_CutPath.Count) + point = trs.TransformPoint(m_CutPath[m_SelectedIndex].position); + + float size = HandleUtility.GetHandleSize(point) * k_HandleSize; + Handles.color = m_CurrentHandleColor; + Handles.DotHandleCap(-1, point, Quaternion.identity, size, evt.type); + } + Handles.color = Color.white; + } + } + + /// + /// Visual indications to help the user: highlighting faces, edges and vertices when snapping on them + /// + void DoVisualCues() + { + if(m_Mesh != null) + { + if(m_TargetFace == null && m_CurrentFace != null) + EditorHandleDrawing.HighlightFaces(m_Mesh, new Face[]{m_CurrentFace}, Color.Lerp(Color.blue, Color.cyan, 0.5f)); + + EditorHandleDrawing.HighlightVertices(m_Mesh, m_SelectedVertices, false); + EditorHandleDrawing.HighlightEdges(m_Mesh, m_SelectedEdges,false); + + if(m_TargetFace != null) + { + if(m_SnapedVertexId != -1) + EditorHandleDrawing.HighlightVertices(m_Mesh, new int[] { m_SnapedVertexId }); + + if(m_SnapedEdge != Edge.Empty) + EditorHandleDrawing.HighlightEdges(m_Mesh, new Edge[] { m_SnapedEdge }); + } + } + } + + /// + /// Display lines of the cut shape + /// + void DoExistingLinesGUI() + { + DrawCutLine(); + DrawMeshConnectionsHandles(); + } + + /// + /// Draw the line corresponding to the current cut path in the face + /// + void DrawCutLine() + { + Handles.color = m_IsCutValid ? k_LineColor : k_InvalidLineColor; + Handles.DrawPolyLine(m_CutPath.Select(tup => m_Mesh.transform.TransformPoint(tup.position)).ToArray()); + Handles.color = Color.white; + } + + /// + /// Draw a helper line between the last point of the cut and the current position of the mouse cursor + /// + void DrawGuideLine() + { + if(m_CurrentPosition.Equals(Vector3.positiveInfinity) || m_ModifyingPoint) + return ; + + if(m_CutPath.Count > 0) + { + Handles.color = k_DrawingLineColor; + Handles.DrawDottedLine(m_Mesh.transform.TransformPoint(m_CutPath[m_CutPath.Count - 1].position), + m_Mesh.transform.TransformPoint(m_CurrentPosition), 5f); + Handles.color = Color.white; + } + } + + /// + /// Draw a helper line to show which vertices of the face are connected to points of the cut shape + /// + void DrawMeshConnectionsHandles() + { + if(m_MeshConnections.Count > 0 && m_Mesh != null) + { + Vertex[] vertices = m_Mesh.GetVertices(); + for (int i = m_MeshConnections.Count - 1; i >= 0; i--) + { + var connection = m_MeshConnections[i]; + if (connection.item1 < 0 || connection.item1 >= m_CutPath.Count + || connection.item2 < 0 || connection.item2 >= vertices.Length) + { + m_MeshConnections.RemoveAt(i); + continue; + } + Handles.color = k_ConnectionsLineColor; + Handles.DrawDottedLine(m_Mesh.transform.TransformPoint(m_CutPath[connection.item1].position), + m_Mesh.transform.TransformPoint(vertices[connection.item2].position), 5f); + Handles.color = Color.white; + } + } + } + } +} diff --git a/Editor/EditorCore/CutTool.UI.cs.meta b/Editor/EditorCore/CutTool.UI.cs.meta new file mode 100644 index 000000000..ef112c5df --- /dev/null +++ b/Editor/EditorCore/CutTool.UI.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b5b10095282c33743955e6e1170c033a \ No newline at end of file diff --git a/Editor/EditorCore/CutTool.Validation.cs b/Editor/EditorCore/CutTool.Validation.cs new file mode 100644 index 000000000..562424c86 --- /dev/null +++ b/Editor/EditorCore/CutTool.Validation.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.ProBuilder; +using Math = UnityEngine.ProBuilder.Math; + +namespace UnityEditor.ProBuilder +{ + partial class CutTool + { + /// + /// Rebuild the line mesh when updated + /// + void RebuildCutShape(bool optimize = true) + { + // If Undo is called immediately after creation this situation can occur + if (m_Mesh == null) + return; + + UpdateMeshConnections(); + ValidateCutShape(); + + // While the vertex count may not change, the triangle winding might. So unfortunately we can't take + // advantage of the `vertexCountChanged = false` optimization here. + ProBuilderEditor.Refresh(); + SceneView.RepaintAll(); + + if(optimize && isALoop && m_IsCutValid) + ExecuteCut(); + + m_Dirty = false; + } + + void ValidateCutShape() + { + Vector3[] verticesPositions = m_Mesh.positionsInternal; + + m_IsCutValid = true; + + //For all segments of the current cut + for(int i = 0; i < m_CutPath.Count-1 && m_IsCutValid; i++) + { + Vector2 segment1Start2D = HandleUtility.WorldToGUIPoint(m_Mesh.transform.TransformPoint(m_CutPath[i].position)); + Vector2 segment1End2D = HandleUtility.WorldToGUIPoint(m_Mesh.transform.TransformPoint(m_CutPath[i+1].position)); + + int lastVertexIndex = (isALoop && i == 0) ? m_CutPath.Count-2 : m_CutPath.Count-1; + //Test intersections with the rest of the cut path + for(int j = i + 2; j < lastVertexIndex && m_IsCutValid; j++) + { + if(((m_CutPath[j].types | m_CutPath[j+1].types) & VertexTypes.VertexInShape) == 0) + { + Vector2 segment2Start2D = + HandleUtility.WorldToGUIPoint(m_Mesh.transform.TransformPoint(m_CutPath[j].position)); + Vector2 segment2End2D = + HandleUtility.WorldToGUIPoint(m_Mesh.transform.TransformPoint(m_CutPath[j + 1].position)); + + m_IsCutValid = !Math.GetLineSegmentIntersect(segment1Start2D, segment1End2D, segment2Start2D, + segment2End2D); + } + } + + if(( (m_CutPath[i].types| m_CutPath[i+1].types) & VertexTypes.VertexInShape ) == 0) + { + //Test intersections with the connections to the face vertices + for(int j = 0; j < m_MeshConnections.Count && m_IsCutValid; j++) + { + SimpleTuple connection = m_MeshConnections[j]; + + if(connection.item1 != i && connection.item1 != i + 1) + { + Vector2 segment2Start2D = + HandleUtility.WorldToGUIPoint( + m_Mesh.transform.TransformPoint(m_CutPath[connection.item1].position)); + Vector2 segment2End2D = + HandleUtility.WorldToGUIPoint( + m_Mesh.transform.TransformPoint(verticesPositions[connection.item2])); + + m_IsCutValid = !Math.GetLineSegmentIntersect(segment1Start2D, segment1End2D, + segment2Start2D, + segment2End2D); + } + } + } + } + + //For all connections to the face vertices + for(int i = 0; i < m_MeshConnections.Count-1 && m_IsCutValid; i++) + { + SimpleTuple connection1 = m_MeshConnections[i]; + Vector2 segment1Start2D = + HandleUtility.WorldToGUIPoint( + m_Mesh.transform.TransformPoint(m_CutPath[connection1.item1].position) ); + Vector2 segment1End2D = + HandleUtility.WorldToGUIPoint( + m_Mesh.transform.TransformPoint(verticesPositions[connection1.item2])); + + //Test intersection with the other connections to the face vertices + for(int j = i+1; j < m_MeshConnections.Count && m_IsCutValid; j++) + { + SimpleTuple connection2 = m_MeshConnections[j]; + + Vector2 segment2Start2D = + HandleUtility.WorldToGUIPoint( + m_Mesh.transform.TransformPoint(m_CutPath[connection2.item1].position)); + Vector2 segment2End2D = + HandleUtility.WorldToGUIPoint( + m_Mesh.transform.TransformPoint(verticesPositions[connection2.item2])); + + m_IsCutValid = !Math.GetLineSegmentIntersect(segment1Start2D, segment1End2D, segment2Start2D, segment2End2D); + } + } + } + + bool CanAppendCurrentPointToPath() + { + int polyCount = m_CutPath.Count; + + if (!Math.IsNumber(m_CurrentPosition)) + return false; + + if (!(polyCount == 0 || m_SelectedIndex != polyCount - 1)) + return false; + + // duplicate points are not permitted, except the special case where placing a final point on the starting + // point finishes the cut operation. + for(int i = 1; i < polyCount; i++) + if (Math.Approx3(m_CutPath[i].position, m_CurrentPosition)) + return false; + + // when the existing vertex count is less than 3, don't allow the special duplicate first vertex position + return polyCount < 2 || !(polyCount < 3 && Math.Approx3(m_CutPath[0].position, m_CurrentPosition)); + } + } +} diff --git a/Editor/EditorCore/CutTool.Validation.cs.meta b/Editor/EditorCore/CutTool.Validation.cs.meta new file mode 100644 index 000000000..6be5f5eca --- /dev/null +++ b/Editor/EditorCore/CutTool.Validation.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 702a2a376d981be4e89240ee50888499 \ No newline at end of file diff --git a/Editor/EditorCore/CutTool.cs b/Editor/EditorCore/CutTool.cs index 72e35b7d5..884f43fdf 100644 --- a/Editor/EditorCore/CutTool.cs +++ b/Editor/EditorCore/CutTool.cs @@ -19,7 +19,7 @@ namespace UnityEditor.ProBuilder { [EditorTool("Cut Tool", typeof(ProBuilderMesh), typeof(PositionToolContext))] - class CutTool : EditorTool + partial class CutTool : EditorTool { ProBuilderMesh m_Mesh; @@ -336,22 +336,6 @@ internal void UpdateTarget() } } - /// - /// Creates a toggle for cut tool overlays. - /// - /// toggle title - /// starting value for the toggle - /// new toggle value - bool DoOverlayToggle(string label, bool val) - { - using(new GUILayout.HorizontalScope()) - { - EditorGUILayout.LabelField(label, GUILayout.Width(225)); - GUILayout.FlexibleSpace(); - return EditorGUILayout.Toggle(val); - } - } - /// /// Main GUI update for the tool, calls every secondary methods to place points, update lines and compute the cut /// @@ -429,83 +413,6 @@ bool IsCursorInSceneView(EditorWindow window) } - /// - /// Overlay GUI - /// - /// the target of this overlay - /// the current SceneView where to display the overlay - void OnOverlayGUI(UObject target, SceneView view) - { - if(MeshSelection.selectedObjectCount != 1) - { - var rect = EditorGUILayout.GetControlRect(false, 45); - EditorGUI.HelpBox(rect, L10n.Tr("One and only one ProBuilder mesh must be selected."), MessageType.Warning); - } - - GUI.enabled = MeshSelection.selectedObjectCount == 1; - - m_RectangleMode = DoOverlayToggle(L10n.Tr("Rectangle Mode"), m_RectangleMode); - EditorPrefs.SetBool(k_RectangleModePrefKey, m_RectangleMode); - - m_SnapToGrid = DoOverlayToggle(L10n.Tr("Snap to Grid"), m_SnapToGrid); - EditorPrefs.SetBool(k_SnapToGridPrefKey, m_SnapToGrid); - - m_SnapToGeometry = DoOverlayToggle(L10n.Tr("Snap to existing edges and vertices"), m_SnapToGeometry); - EditorPrefs.SetBool(k_SnapToGeometryPrefKey, m_SnapToGeometry); - - if(m_RectangleMode) - { - EditorGUI.indentLevel++; - using(new GUILayout.HorizontalScope()) - { - EditorGUILayout.LabelField(L10n.Tr("Click & drag to draw a rectangle cut"), GUILayout.Width(250)); - } - EditorGUI.indentLevel--; - } - - if(!m_RectangleMode && !m_SnapToGeometry) - GUI.enabled = false; - EditorGUI.indentLevel++; - using(new GUILayout.HorizontalScope()) - { - EditorGUILayout.LabelField(L10n.Tr("Snapping distance"), GUILayout.Width(200)); - m_SnappingDistance = EditorGUILayout.FloatField(m_SnappingDistance); - EditorPrefs.SetFloat( k_SnappingDistancePrefKey, m_SnappingDistance); - } - EditorGUI.indentLevel--; - - GUI.enabled = true; - - if(MeshSelection.selectedObjectCount != 1) - GUI.enabled = false; - - using(new GUILayout.HorizontalScope()) - { - if(m_Mesh == null) - { - if(GUILayout.Button(EditorGUIUtility.TrTextContent("Start"))) - UpdateTarget(); - - if(GUILayout.Button(EditorGUIUtility.TrTextContent("Quit"))) - ExitTool(); - } - else - { - if(!m_RectangleMode && m_CutPath.Count > 1) - { - if(GUILayout.Button(EditorGUIUtility.TrTextContent("Complete"))) - ExecuteCut(); - } - - if(GUILayout.Button(EditorGUIUtility.TrTextContent("Cancel"))) - ExitTool(); - } - } - - GUI.enabled = true; - } - - /// /// Handle key events /// @@ -635,236 +542,8 @@ void DoPointPlacement(EditorWindow window) //If nothing in the current cut, then pass the mouse event to ProBuilder Editor to handle selection. //This might disable the tool depending on the new selection. - if(m_CutPath.Count == 0 - && !hasHitPosition - && HandleUtility.nearestControl == m_ControlId) - { - ProBuilderEditor.instance.HandleMouseEvent(SceneView.lastActiveSceneView, m_ControlId); - } - } - - /// - /// Rectangle mode: click and drag to define a rectangular cut on the face. - /// On mouse up, auto-places the 4 corners and executes the cut. - /// - void DoRectanglePlacement(EditorWindow window) - { - Event evt = Event.current; - EventType evtType = evt.type; - - m_SnappingPoint = m_SnapToGeometry || (evt.modifiers & EventModifiers.Control) != 0; - m_ModifyingPoint = false; - - bool hasHitPosition = UpdateHitPosition(); - - // Visual helpers - if (evtType == EventType.Repaint) - { - if (hasHitPosition && IsCursorInSceneView(window)) - { - m_CurrentCutCursor = m_CutCursorTexture; - m_CurrentHandleColor = k_HandleColorAddNewVertex; - } - else - { - m_CurrentCutCursor = null; - m_CurrentPosition = Vector3.positiveInfinity; - } - } - - // Mouse down: start rectangle drag - if (hasHitPosition - && evtType == EventType.MouseDown && evt.button == 0 - && HandleUtility.nearestControl == m_ControlId - && !m_RectDragging) - { - m_RectDragging = true; - m_RectStartPoint = m_CurrentPosition; - m_RectEndPoint = m_CurrentPosition; - m_TargetFace = m_CurrentFace; - - var edges = m_TargetFace.edges; - m_SelectedVertices = edges.Select(e => e.a).ToArray(); - m_SelectedEdges = edges.ToArray(); - - m_CutPath.Clear(); - m_MeshConnections.Clear(); - evt.Use(); - } - - // Mouse drag: update rectangle end point - if (m_RectDragging && evtType == EventType.MouseDrag && evt.button == 0) - { - if (hasHitPosition && m_CurrentFace == m_TargetFace) - { - m_RectEndPoint = m_CurrentPosition; - } - evt.Use(); - } - - // Mouse up: finalize the rectangle and execute cut - if (m_RectDragging - && (evtType == EventType.MouseUp && evt.button == 0)) - { - m_RectDragging = false; - - // Project start/end onto the face plane to compute the other 2 corners - Vector3 start = m_RectStartPoint; - Vector3 end = m_RectEndPoint; - - // Compute face normal for projection - Vector3 faceNormal = Math.Normal(m_Mesh, m_TargetFace); - - // Create a local coordinate system on the face plane - Vector3 faceRight, faceUp; - if (Mathf.Abs(Vector3.Dot(faceNormal, Vector3.up)) > 0.99f) - faceRight = Vector3.Cross(faceNormal, Vector3.forward).normalized; - else - faceRight = Vector3.Cross(faceNormal, Vector3.up).normalized; - faceUp = Vector3.Cross(faceNormal, faceRight).normalized; - - // Decompose rect diagonals in face space - Vector3 diagonal = end - start; - float rightDot = Vector3.Dot(diagonal, faceRight); - float upDot = Vector3.Dot(diagonal, faceUp); - - // Compute the 4 rectangle corners in world space - Vector3 corner0 = start; - Vector3 corner1 = start + faceRight * rightDot; - Vector3 corner2 = end; - Vector3 corner3 = start + faceUp * upDot; - - // Snap all corners to grid if enabled - if (m_SnapToGrid) - { - corner0 = ProBuilderSnapping.Snap(corner0, EditorSnapping.activeMoveSnapValue); - corner1 = ProBuilderSnapping.Snap(corner1, EditorSnapping.activeMoveSnapValue); - corner2 = ProBuilderSnapping.Snap(corner2, EditorSnapping.activeMoveSnapValue); - corner3 = ProBuilderSnapping.Snap(corner3, EditorSnapping.activeMoveSnapValue); - } - - if (HasSignificantRectangle(corner0, corner2)) - { - // Build cut path: 4 corners + close back to start to form a loop - UndoUtility.RecordObject(this, "Rectangle Cut"); - - m_CurrentPosition = corner0; - m_CurrentPositionNormal = faceNormal; - m_CurrentVertexTypes = VertexTypes.NewVertex; - m_CurrentFace = m_TargetFace; - AddCurrentPositionToPath(false); - - m_CurrentPosition = corner1; - m_CurrentPositionNormal = faceNormal; - m_CurrentVertexTypes = VertexTypes.NewVertex; - AddCurrentPositionToPath(false); - - m_CurrentPosition = corner2; - m_CurrentPositionNormal = faceNormal; - m_CurrentVertexTypes = VertexTypes.NewVertex; - AddCurrentPositionToPath(false); - - m_CurrentPosition = corner3; - m_CurrentPositionNormal = faceNormal; - m_CurrentVertexTypes = VertexTypes.NewVertex; - AddCurrentPositionToPath(false); - - // Close the loop by returning to the start corner - m_CurrentPosition = corner0; - m_CurrentPositionNormal = faceNormal; - m_CurrentVertexTypes = VertexTypes.VertexInShape; - m_CurrentFace = m_TargetFace; - AddCurrentPositionToPath(false); - - // RebuildCutShape(true) already executes the cut if the path is valid - // Don't call DoCut() again — it would run on already-cleared data - RebuildCutShape(false); - - if (m_CutPath.Count >= 2 && m_IsCutValid) - { - ActionResult result = DoCut(); - EditorUtility.ShowNotification(result.notification); - } - } - - m_RectStartPoint = Vector3.positiveInfinity; - m_RectEndPoint = Vector3.positiveInfinity; - evt.Use(); - } - - // Pass through to ProBuilder selection if not interacting - if(!m_RectDragging - && m_CutPath.Count == 0 - && !hasHitPosition - && HandleUtility.nearestControl == m_ControlId) - { - ProBuilderEditor.instance.HandleMouseEvent(SceneView.lastActiveSceneView, m_ControlId); - } - } - - bool HasSignificantRectangle(Vector3 start, Vector3 end) - { - return Vector3.Distance(start, end) > 0.001f; - } - - /// - /// Draw the rectangle preview during a drag operation. - /// - void DoRectanglePreview() - { - if (!m_RectDragging || m_Mesh == null || m_TargetFace == null) + if (TryPassThroughSelection(window, hasHitPosition)) return; - - Transform trs = m_Mesh.transform; - Vector3 startW = trs.TransformPoint(m_RectStartPoint); - Vector3 endW = trs.TransformPoint(m_RectEndPoint); - - // Compute face plane - Vector3 faceNormal = Math.Normal(m_Mesh, m_TargetFace); - - Vector3 faceRight, faceUp; - if (Mathf.Abs(Vector3.Dot(faceNormal, Vector3.up)) > 0.99f) - faceRight = Vector3.Cross(faceNormal, Vector3.forward).normalized; - else - faceRight = Vector3.Cross(faceNormal, Vector3.up).normalized; - faceUp = Vector3.Cross(faceNormal, faceRight).normalized; - - Vector3 diagonal = endW - startW; - float rightDot = Vector3.Dot(diagonal, faceRight); - float upDot = Vector3.Dot(diagonal, faceUp); - - Vector3 c0 = startW; - Vector3 c1 = startW + faceRight * rightDot; - Vector3 c2 = endW; - Vector3 c3 = startW + faceUp * upDot; - - // Draw filled rectangle - Handles.color = k_RectPreviewColor; - Handles.DrawAAConvexPolygon(new Vector3[] { c0, c1, c2, c3 }); - - // Draw outline - Handles.color = k_RectOutlineColor; - Handles.DrawAAPolyLine(2f, new Vector3[] { c0, c1, c2, c3, c0 }); - } - - bool CanAppendCurrentPointToPath() - { - int polyCount = m_CutPath.Count; - - if (!Math.IsNumber(m_CurrentPosition)) - return false; - - if (!(polyCount == 0 || m_SelectedIndex != polyCount - 1)) - return false; - - // duplicate points are not permitted, except the special case where placing a final point on the starting - // point finishes the cut operation. - for(int i = 1; i < polyCount; i++) - if (Math.Approx3(m_CutPath[i].position, m_CurrentPosition)) - return false; - - // when the existing vertex count is less than 3, don't allow the special duplicate first vertex position - return polyCount < 2 || !(polyCount < 3 && Math.Approx3(m_CutPath[0].position, m_CurrentPosition)); } internal void AddCurrentPositionToPath(bool optimize = true) @@ -886,1049 +565,5 @@ internal void AddCurrentPositionToPath(bool optimize = true) RebuildCutShape(optimize); } - - /// - /// Compute the position designated by the user in the current mesh/face taking into account snapping - /// - /// true is a valid position is computed in the mesh - bool UpdateHitPosition() - { - Event evt = Event.current; - - Ray ray = UHandleUtility.GUIPointToWorldRay(evt.mousePosition); - RaycastHit pbHit; - - m_CurrentFace = null; - - if (UnityEngine.ProBuilder.HandleUtility.FaceRaycast(ray, m_Mesh, out pbHit)) - { - UpdateCurrentPosition(m_Mesh.faces[pbHit.face], pbHit.point ,pbHit.normal); - return true; - } - - return false; - } - - /// - /// Add the position in the face to the cut taking into account snapping - /// - internal void UpdateCurrentPosition(Face face, Vector3 position, Vector3 normal) - { - m_CurrentPosition = position; - m_CurrentPositionNormal = normal; - m_CurrentFace = face; - m_CurrentVertexTypes = VertexTypes.None; - - CheckPointInCutPath(); - - if (m_CurrentVertexTypes == VertexTypes.None && !m_ModifyingPoint) - CheckPointInMesh(); - - // Only apply grid snap if the point is a free-floating new vertex on the face - // (don't pull already-snapped edge/vertex points off their targets) - if (m_CurrentVertexTypes == VertexTypes.NewVertex || m_CurrentVertexTypes == VertexTypes.None) - ApplyGridSnap(); - } - - /// - /// Snap the current position to Unity's grid if grid snapping is enabled. - /// - void ApplyGridSnap() - { - if (m_SnapToGrid) - m_CurrentPosition = ProBuilderSnapping.Snap(m_CurrentPosition, EditorSnapping.activeMoveSnapValue); - } - - /// - /// Updates the connections between the cut path and the mesh vertices - /// - internal void UpdateMeshConnections() - { - m_MeshConnections.Clear(); - if(m_CutPath.Count < 2) - return; - - List existingVerticesInCut = - m_CutPath.Where(v => ( v.types | VertexTypes.ExistingVertex ) != 0) - .Select(v => v.position).ToList(); - - Vector3[] verticesPositions = m_Mesh.positionsInternal; - if(!isALoop) - { - //Connects to start and the end of the path to create a loop - float minDistToStart = Single.PositiveInfinity, minDistToStart2 = Single.PositiveInfinity; - float minDistToEnd = Single.PositiveInfinity, minDistToEnd2 = Single.PositiveInfinity; - int bestVertexIndexToStart = -1, bestVertexIndexToStart2 = -1, bestVertexIndexToEnd = -1, bestVertexIndexToEnd2 = -1; - float dist; - foreach(var vertexIndex in m_TargetFace.distinctIndexes) - { - if(existingVerticesInCut.Count > 0) - { - if(existingVerticesInCut.Exists(vert => Math.Approx3(verticesPositions[vertexIndex], vert))) - continue; - } - - if(( m_CutPath[0].types & VertexTypes.NewVertex ) != 0) - { - dist = Vector3.Distance(verticesPositions[vertexIndex], m_CutPath[0].position); - if(dist < minDistToStart) - { - minDistToStart2 = minDistToStart; - bestVertexIndexToStart2 = bestVertexIndexToStart; - minDistToStart = dist; - bestVertexIndexToStart = vertexIndex; - }else if(dist < minDistToStart2) - { - minDistToStart2 = dist; - bestVertexIndexToStart2 = vertexIndex; - } - } - if(m_CutPath.Count > 1 && ( m_CutPath[m_CutPath.Count - 1].types & VertexTypes.NewVertex ) != 0) - { - dist = Vector3.Distance(verticesPositions[vertexIndex], m_CutPath[m_CutPath.Count - 1].position); - if(dist < minDistToEnd) - { - minDistToEnd2 = minDistToEnd; - bestVertexIndexToEnd2 = bestVertexIndexToEnd; - minDistToEnd = dist; - bestVertexIndexToEnd = vertexIndex; - } - else if(dist < minDistToEnd2) - { - minDistToEnd2 = dist; - bestVertexIndexToEnd2 = vertexIndex; - } - } - } - - //Do not connect the 2 extremities to the same point - if(bestVertexIndexToStart == bestVertexIndexToEnd) - { - if(minDistToStart2 < minDistToEnd2) - bestVertexIndexToStart = bestVertexIndexToStart2; - else - bestVertexIndexToEnd = bestVertexIndexToEnd2; - } - - if(bestVertexIndexToStart >= 0) - m_MeshConnections.Add(new SimpleTuple(0,bestVertexIndexToStart)); - - if(bestVertexIndexToEnd >= 0) - m_MeshConnections.Add(new SimpleTuple(m_CutPath.Count - 1,bestVertexIndexToEnd)); - } - else if(isALoop && connectionsToBordersCount < 2) - { - //The path must have minimum connections with the face borders, find the closest vertices - foreach(var vertexIndex in m_TargetFace.distinctIndexes) - { - if(existingVerticesInCut.Count > 0) - { - if(existingVerticesInCut.Exists(vert => Math.Approx3(verticesPositions[vertexIndex], vert))) - continue; - } - - int pathIndex = -1; - float minDistance = Single.MaxValue; - for(int i = 0; i < m_CutPath.Count; i++) - { - if(( m_CutPath[i].types & (VertexTypes.AddedOnEdge | VertexTypes.ExistingVertex) ) == 0) - { - float dist = Vector3.Distance(verticesPositions[vertexIndex], m_CutPath[i].position); - if(dist < minDistance) - { - minDistance = dist; - pathIndex = i; - } - } - } - - if(pathIndex >= 0) - { - if(m_MeshConnections.Exists(tup => tup.item1 == pathIndex)) - { - var tuple = m_MeshConnections.Find(tup => tup.item1 == pathIndex); - if(Vector3.Distance(m_CutPath[tuple.item1].position, verticesPositions[tuple.item2]) - > Vector3.Distance(m_CutPath[pathIndex].position, verticesPositions[vertexIndex])) - { - m_MeshConnections.Remove(tuple); - m_MeshConnections.Add(new SimpleTuple(pathIndex, vertexIndex)); - } - } - else - m_MeshConnections.Add(new SimpleTuple(pathIndex, vertexIndex)); - } - } - - m_MeshConnections.Sort((a,b) => - (int)Mathf.Sign(Vector3.Distance(m_CutPath[a.item1].position, verticesPositions[a.item2]) - - Vector3.Distance(m_CutPath[b.item1].position, verticesPositions[b.item2]))); - - int connectionsCount = 2 - connectionsToBordersCount; - m_MeshConnections.RemoveRange(connectionsCount,m_MeshConnections.Count - connectionsCount); - } - } - - /// - /// Compute the cut result and display a notification - /// - void ExecuteCut(bool restorePrevious = true) - { - ActionResult result = DoCut(); - EditorUtility.ShowNotification(result.notification); - - if(restorePrevious) - ExitTool(); - } - - /// - /// Compute the faces resulting from the cut: - /// - First inserts points defining the cut as vertices in the face - /// - Compute the central polygon is the cut is creating a closed polygon in the face - /// - Update the rest of the face accordingly to the cut and the central polygon - /// - /// ActionResult success if it was possible to create the cut - internal ActionResult DoCut() - { - if (m_TargetFace == null || m_CutPath.Count < 2) - { - return new ActionResult(ActionResult.Status.Canceled, L10n.Tr("Not enough elements selected for a cut")); - } - - if(!m_IsCutValid) - { - return new ActionResult(ActionResult.Status.Failure, L10n.Tr("The current cut overlaps itself")); - } - - UndoUtility.RecordObject(m_Mesh, "Execute Cut"); - - List meshVertices = new List(); - m_Mesh.GetVerticesInList(meshVertices); - Vertex[] formerVertices = new Vertex[m_MeshConnections.Count]; - for(int i = 0; i < m_MeshConnections.Count; i++) - { - formerVertices[i] = meshVertices[m_MeshConnections[i].item2]; - } - - //Insert cut vertices in the mesh - List cutVertices = InsertVertices(); - m_Mesh.GetVerticesInList(meshVertices); - //Retrieve indexes of the cut points in the mesh vertices - int[] cutIndexes = cutVertices.Select(vert => meshVertices.IndexOf(vert)).ToArray(); - - //Update mesh connections with new indexes - for(int i = 0; i connection = m_MeshConnections[i]; - connection.item1 = meshVertices.IndexOf(cutVertices[connection.item1]); - connection.item2 = meshVertices.IndexOf(formerVertices[i]); - m_MeshConnections[i] = connection; - } - - List newFaces = new List(); - // If the cut defines a loop in the face, create the polygon corresponding to that loop - if (isALoop) - { - Face f = m_Mesh.CreatePolygon(cutIndexes, false); - - if(f == null) - return new ActionResult(ActionResult.Status.Failure, L10n.Tr("Cut Shape is not valid")); - - ApplySourceFaceSettings(f, m_TargetFace); - - Vector3 nrm = Math.Normal(m_Mesh, f); - Vector3 targetNrm = Math.Normal(m_Mesh, m_TargetFace); - // If the shape is define in the wrong orientation compared to the former face, reverse it - if(Vector3.Dot(nrm,targetNrm) < 0f) - f.Reverse(); - - newFaces.Add(f); - } - - //Compute the rest of the new faces (faces outside of the loop or division of the original face) - List faces = ComputeNewFaces(m_TargetFace, cutIndexes); - if(!isALoop) - newFaces.AddRange(faces); - - //Remove inserted vertices only if they were inserted for the process - List verticesIndexesToDelete = new List(); - for(int i = 0; i < m_CutPath.Count; i++) - { - if(( m_CutPath[i].types & VertexTypes.NewVertex ) != 0 - && ( m_CutPath[i].types & VertexTypes.VertexInShape ) == 0) - verticesIndexesToDelete.Add(cutIndexes[i]); - } - m_Mesh.DeleteVertices(verticesIndexesToDelete); - - //Delete former face - m_Mesh.DeleteFace(m_TargetFace); - - m_Mesh.ToMesh(); - m_Mesh.Refresh(); - m_Mesh.Optimize(); - - //Update mesh selection after the cut has been performed - MeshSelection.ClearElementSelection(); - m_Mesh.SetSelectedFaces(newFaces); - ProBuilderEditor.Refresh(); - - ResetToolState(true); - - return new ActionResult(ActionResult.Status.Success, L10n.Tr("Cut executed")); - } - - - /// - /// Based on the new vertices inserted in the face, this method computes the different faces - /// created between the cut and the original face (external to the cut if it makes a loop) - /// - /// The faces are created by parsing the edges that defines the border of the original face. Is an edge ends on a - /// vertex that is part of the cut, or belongs to a connection between the cut and the face, - /// we close the defined polygon using the cut (though ComputeFaceClosure method) and create a face out of this polygon - /// - /// Original face to modify - /// Indexes of the new vertices inserted in the face - /// The list of polygons to create (defined by their vertices indexes) - List ComputeNewFaces(Face face, IList cutVertexIndexes) - { - List newFaces = new List(); - - //Get Vertices from the mesh - Dictionary sharedToUnique = m_Mesh.sharedVertexLookup; - var cutVertexSharedIndexes = cutVertexIndexes.Select(ind => sharedToUnique[ind]).ToList(); - - //Parse peripheral edges to unique id and find a common point between the peripheral edges and the cut - var peripheralEdges = WingedEdge.SortEdgesByAdjacency(face); - var peripheralEdgesUnique = new List(); - int startIndex = -1; - for (int i = 0; i < peripheralEdges.Count; i++) - { - Edge eShared = peripheralEdges[i]; - Edge eUnique = new Edge(sharedToUnique[eShared.a], sharedToUnique[eShared.b]); - peripheralEdgesUnique.Add(eUnique); - - if (startIndex == -1 && ( cutVertexSharedIndexes.Contains(eUnique.a) - || m_MeshConnections.Exists(tup => sharedToUnique[tup.item2] == eUnique.a))) - startIndex = i; - } - - //Create a polygon for each cut reaching the mesh edges - List facesToDelete = new List(); - List polygon = new List(); - for (int i = startIndex; i <= peripheralEdgesUnique.Count + startIndex; i++) - { - polygon.Add(peripheralEdges[i % peripheralEdgesUnique.Count].a); - Edge e = peripheralEdgesUnique[i % peripheralEdgesUnique.Count]; - - if(polygon.Count > 1) - { - int index = -1; - if(cutVertexSharedIndexes.Contains(e.a)) // get next vertex - { - index = e.a; - } - else if(m_MeshConnections.Exists(tup => sharedToUnique[tup.item2] == e.a)) - { - SimpleTuple connection = m_MeshConnections.Find(tup => sharedToUnique[tup.item2] == e.a); - polygon.Add(connection.item1); - index = sharedToUnique[connection.item1]; - } - - if(index >= 0) - { - // In the case of only 2 distinct, a face should not be added. - if (polygon.Count != 2) - { - List toDelete; - Face newFace = ComputeFaceClosure(polygon, index, cutVertexSharedIndexes, out toDelete); - if (newFace != null && newFace.indexesInternal != null) - { - ApplySourceFaceSettings(newFace, m_TargetFace); - newFaces.Add(newFace); - facesToDelete.AddRange(toDelete); - } - } - - //Start a new polygon - polygon = new List(); - polygon.Add(peripheralEdges[i % peripheralEdgesUnique.Count].a); - } - } - } - polygon.Clear(); - - m_Mesh.DeleteFaces(facesToDelete); - return newFaces; - } - - - - /// - /// The method computes all the possible faces that can be made starting by the vertices in polygonStart and ending with the cut - /// This method creates faces that are not the final one and that must be deleted at the end. These invalid faces are returned in facesToDelete - /// The only valid face is returned from this method. From all defined faces, the valid face is the one with the smaller area - /// (otherwise it means it covers another face of the mesh). - /// - /// Indexes of the first vertices of the new Face to define, these vertices are coming from the original face only - /// Current vertex index in the cut - /// Indexes of the vertices defining the cut - /// out : extra faces created by this method that will need to be deleted after - /// (these faces cannot be deleted directly as it will break the m_MeshConnections by deleting some indexes before the end of the algorithm) - /// the valid face that need to be kept in the resulting mesh - Face ComputeFaceClosure( List polygonStart, int currentIndex, List cutIndexes, out List facesToDelete) - { - List meshVertices = new List(); - IList uniqueIdToVertexIndex = m_Mesh.sharedVertices; - Dictionary sharedToUnique = m_Mesh.sharedVertexLookup; - - facesToDelete = new List(); - - if (polygonStart == null || polygonStart.Count == 0 || cutIndexes == null || cutIndexes.Count == 0) - return null; - - int polygonFirstVertex = polygonStart[0]; - int startIndex = cutIndexes.IndexOf(currentIndex); - - if (startIndex < 0 || !sharedToUnique.ContainsKey(polygonFirstVertex)) - return null; - - int polygonFirstSharedIndex = sharedToUnique[polygonFirstVertex]; - - bool hasConnection = m_MeshConnections.Exists(tup => sharedToUnique.ContainsKey(tup.item2) - && sharedToUnique[tup.item2] == polygonFirstSharedIndex); - SimpleTuple connection = default; - - if (hasConnection) - connection = m_MeshConnections.Find(tup => sharedToUnique.ContainsKey(tup.item2) - && sharedToUnique[tup.item2] == polygonFirstSharedIndex); - - List> closureCandidates = new List>(); - - //Go through the cut in reverse direction - int index; - int finalIndex = isALoop ?(startIndex - cutIndexes.Count) : 0; - bool connected = false; - List candidate = new List(); - for(index = startIndex - 1; index >= finalIndex; index--) - { - int vertexIndex = uniqueIdToVertexIndex[cutIndexes[(index + cutIndexes.Count) % cutIndexes.Count]][0]; - candidate.Add(vertexIndex); - if(sharedToUnique[vertexIndex] == polygonFirstSharedIndex || - (hasConnection && sharedToUnique.ContainsKey(connection.item1) - && sharedToUnique[vertexIndex] == sharedToUnique[connection.item1])) - { - connected = true; - break; - } - } - - //If we find a valid candidate for the connection, add it to the list - if(connected) - closureCandidates.Add(candidate); - - //Go through the cut in forward direction - finalIndex = isALoop ? (startIndex + cutIndexes.Count) : cutIndexes.Count; - connected = false; - candidate = new List(); - for(index = startIndex + 1; index < finalIndex; index++) - { - int vertexIndex = uniqueIdToVertexIndex[cutIndexes[index % cutIndexes.Count]][0]; - candidate.Add(vertexIndex); - if(sharedToUnique[vertexIndex] == polygonFirstSharedIndex || - (hasConnection && sharedToUnique.ContainsKey(connection.item1) - && sharedToUnique[vertexIndex] == sharedToUnique[connection.item1])) - { - connected = true; - break; - } - } - - //If we find a valid candidate for the connection, add it to the list - if(connected) - closureCandidates.Add(candidate); - - //Go through the different candidate and keep the best one - Face bestFace = null; - float bestArea = 0f; - foreach(var closure in closureCandidates) - { - closure.AddRange(polygonStart); - - Face face = m_Mesh.CreatePolygon(closure, false); - meshVertices.Clear(); - m_Mesh.GetVerticesInList(meshVertices); - uniqueIdToVertexIndex = m_Mesh.sharedVertices; - sharedToUnique = m_Mesh.sharedVertexLookup; - - float area; - if (!TryGetFaceArea(face, meshVertices, uniqueIdToVertexIndex, sharedToUnique, out area)) - { - if (face != null) - facesToDelete.Add(face); - - continue; - } - - if(bestFace != null) - { - if(area < bestArea) - { - facesToDelete.Add(bestFace); - bestArea = area; - bestFace = face; - } - else - facesToDelete.Add(face); - } - else - { - bestFace = face; - bestArea = area; - } - } - - return bestFace; - } - - void ApplySourceFaceSettings(Face destination, Face source) - { - if (destination == null || source == null) - return; - - destination.submeshIndex = source.submeshIndex; - destination.manualUV = source.manualUV; - destination.uv = new AutoUnwrapSettings(source.uv); - destination.textureGroup = source.textureGroup; - destination.smoothingGroup = source.smoothingGroup; - destination.elementGroup = source.elementGroup; - } - - bool TryGetFaceArea(Face face, List meshVertices, IList uniqueIdToVertexIndex, - Dictionary sharedToUnique, out float area) - { - area = 0f; - - if (face == null || face.indexesInternal == null) - return false; - - Vector3[] vertices = meshVertices.Select(vertex => vertex.position).ToArray(); - int[] indexes = new int[face.indexesInternal.Length]; - - for (int i = 0; i < face.indexesInternal.Length; i++) - { - int uniqueIndex; - if (!sharedToUnique.TryGetValue(face.indexesInternal[i], out uniqueIndex)) - return false; - - if (uniqueIndex < 0 || uniqueIndex >= uniqueIdToVertexIndex.Count) - return false; - - SharedVertex sharedVertex = uniqueIdToVertexIndex[uniqueIndex]; - if (sharedVertex == null || sharedVertex.Count < 1) - return false; - - indexes[i] = sharedVertex[0]; - } - - area = Math.PolygonArea(vertices, indexes); - return true; - } - - /// - /// Check whether the current position (m_CurrentPosition) can be associated/snapped to an existing position of the path - /// - void CheckPointInCutPath() - { - //Check if trying to reach the start point - if(!m_ModifyingPoint && m_CutPath.Count > 1) - { - float snapDistance = 0.1f; - var vertexData = m_CutPath[0]; - if(Math.Approx3(vertexData.position, m_CurrentPosition, snapDistance)) - { - m_CurrentPosition = vertexData.position; - m_CurrentVertexTypes = vertexData.types | VertexTypes.VertexInShape; - m_SelectedIndex = 0; - } - } - else if (m_SnappingPoint || m_ModifyingPoint) - { - float snapDistance = m_SnappingDistance; - for(int i = 0; i < m_CutPath.Count; i++) - { - var vertexData = m_CutPath[i]; - if(Math.Approx3(vertexData.position, - m_CurrentPosition, - snapDistance)) - { - snapDistance = Vector3.Distance(vertexData.position, m_CurrentPosition); - if(!m_ModifyingPoint) - m_CurrentPosition = vertexData.position; - m_CurrentVertexTypes = vertexData.types | VertexTypes.VertexInShape; - m_SelectedIndex = i; - } - } - } - } - - /// - /// Check whether the current position (m_CurrentPosition) can be associated/snapped to an existing - /// edge or vertex of the current face - /// - void CheckPointInMesh() - { - m_CurrentVertexTypes = VertexTypes.NewVertex; - bool snapedOnVertex = false; - float snapDistance = m_SnappingDistance; - int bestIndex = -1; - float bestDistance = Mathf.Infinity; - - m_SnapedVertexId = -1; - m_SnapedEdge = Edge.Empty; - - Vertex[] vertices = m_Mesh.GetVertices(); - List peripheralEdges = WingedEdge.SortEdgesByAdjacency(m_CurrentFace); - if (m_TargetFace != null && m_CurrentFace != m_TargetFace) - peripheralEdges = WingedEdge.SortEdgesByAdjacency(m_TargetFace); - for (int i = 0; i < peripheralEdges.Count; i++) - { - if ((m_TargetFace == null || m_TargetFace == m_CurrentFace) && m_SnappingPoint) - { - if (Math.Approx3(vertices[peripheralEdges[i].a].position, - m_CurrentPosition, - snapDistance)) - { - bestIndex = i; - snapedOnVertex = true; - break; - } - else - { - float dist = Math.DistancePointLineSegment( - m_CurrentPosition, - vertices[peripheralEdges[i].a].position, - vertices[peripheralEdges[i].b].position); - - if (dist < Mathf.Min(snapDistance, bestDistance)) - { - bestIndex = i; - bestDistance = dist; - } - } - } - //Even with no snapping, try to detect if the first point is on a existing geometry - else if(m_TargetFace == null && !m_SnappingPoint) - { - if (Math.Approx3(vertices[peripheralEdges[i].a].position, - m_CurrentPosition, - 0.01f)) - { - bestIndex = i; - snapedOnVertex = true; - break; - } - else - { - float dist = Math.DistancePointLineSegment( - m_CurrentPosition, - vertices[peripheralEdges[i].a].position, - vertices[peripheralEdges[i].b].position); - - if (dist < Mathf.Min(0.01f, bestDistance)) - { - bestIndex = i; - bestDistance = dist; - } - } - } - else if(m_CurrentFace != m_TargetFace && m_TargetFace != null ) - { - float edgeDist = Math.DistancePointLineSegment(m_CurrentPosition, - vertices[peripheralEdges[i].a].position, - vertices[peripheralEdges[i].b].position); - - float vertexDist = Vector3.Distance(m_CurrentPosition, - vertices[peripheralEdges[i].a].position); - - if (edgeDist < vertexDist && edgeDist < bestDistance) - { - bestIndex = i; - bestDistance = edgeDist; - snapedOnVertex = false; - } - //always prioritize vertex snap on edge snap - else if (vertexDist <= bestDistance) - { - bestIndex = i; - bestDistance = vertexDist; - snapedOnVertex = true; - } - } - } - - //We found a close vertex - if (snapedOnVertex) - { - m_CurrentPosition = vertices[peripheralEdges[bestIndex].a].position; - m_CurrentVertexTypes = VertexTypes.ExistingVertex; - m_SelectedIndex = -1; - - m_SnapedVertexId = peripheralEdges[bestIndex].a; - CheckPointInCutPath(); - } - //If not, did we found a close edge? - else if (bestIndex >= 0) - { - if (m_TargetFace == null || m_TargetFace == m_CurrentFace) - { - Vector3 left = vertices[peripheralEdges[bestIndex].a].position, - right = vertices[peripheralEdges[bestIndex].b].position; - - float x = (m_CurrentPosition - left).magnitude; - float y = (m_CurrentPosition - right).magnitude; - - m_CurrentPosition = left + (x / (x + y)) * (right - left); - } - else //if(m_CurrentFace != m_TargetFace) - { - Vector3 a = m_CurrentPosition - - vertices[peripheralEdges[bestIndex].a].position; - Vector3 b = vertices[peripheralEdges[bestIndex].b].position - - vertices[peripheralEdges[bestIndex].a].position; - - float angle = Vector3.Angle(b, a); - m_CurrentPosition = Vector3.Magnitude(a) * Mathf.Cos(angle * Mathf.Deg2Rad) * b / Vector3.Magnitude(b); - m_CurrentPosition += vertices[peripheralEdges[bestIndex].a].position; - } - - m_SnapedEdge = peripheralEdges[bestIndex]; - - m_CurrentVertexTypes = VertexTypes.AddedOnEdge; - m_SelectedIndex = -1; - } - } - - /// - /// Insert all position from the cut path to the current faces as new vertices - /// - /// The list of Vertex inserted in the face - List InsertVertices() - { - List newVertices = new List(); - - foreach (var vertexData in m_CutPath) - { - switch (vertexData.types) - { - case VertexTypes.ExistingVertex: - case VertexTypes.VertexInShape: - newVertices.Add(InsertVertexOnExistingVertex(vertexData.position)); - break; - case VertexTypes.AddedOnEdge: - newVertices.Add(InsertVertexOnExistingEdge(vertexData.position)); - break; - case VertexTypes.NewVertex: - newVertices.Add(m_Mesh.InsertVertexInMesh(vertexData.position,vertexData.normal)); - break; - default: - break; - } - } - - return newVertices; - } - - /// - /// Method to retrieve a vertex already existing in the face to avoid duplicated - /// - /// The vertex position - /// The retrieved vertex - Vertex InsertVertexOnExistingVertex(Vector3 vertexPosition) - { - Vertex vertex = null; - - List vertices = m_Mesh.GetVertices().ToList(); - for (int vertIndex = 0; vertIndex < vertices.Count; vertIndex++) - { - if (Math.Approx3(vertices[vertIndex].position, vertexPosition) - && !float.IsNaN(vertices[vertIndex].normal.x) ) - { - vertex = vertices[vertIndex]; - break; - } - } - - return vertex; - } - - /// - /// Insert the vertex in an exiting edge - /// - /// The position of the vertex to insert - /// The inew vertex inserted - Vertex InsertVertexOnExistingEdge(Vector3 vertexPosition) - { - List vertices = m_Mesh.GetVertices().ToList(); - List peripheralEdges = WingedEdge.SortEdgesByAdjacency(m_TargetFace); - - int bestIndex = -1; - float bestDistance = Mathf.Infinity; - for (int i = 0; i < peripheralEdges.Count; i++) - { - float dist = UnityEngine.ProBuilder.Math.DistancePointLineSegment(vertexPosition, - vertices[peripheralEdges[i].a].position, - vertices[peripheralEdges[i].b].position); - - if (dist < bestDistance) - { - bestIndex = i; - bestDistance = dist; - } - } - - Vertex v = m_Mesh.InsertVertexOnEdge(peripheralEdges[bestIndex], vertexPosition); - return v; - } - - /// - /// Display existing points of the cut - /// - void DoExistingPointsGUI() - { - Transform trs = m_Mesh.transform; - int len = m_CutPath.Count; - - Event evt = Event.current; - - if (evt.type == EventType.Repaint) - { - for (int index = 0; index < len; index++) - { - Vector3 point = trs.TransformPoint(m_CutPath[index].position); - float size = HandleUtility.GetHandleSize(point) * k_HandleSize; - - Handles.color = k_HandleColor; - Handles.DotHandleCap(-1, point, Quaternion.identity, size, evt.type); - } - - Handles.color = Color.white; - } - } - - /// - /// Display current position to potentially add to the cut - /// - void DoCurrentPointsGUI() - { - Transform trs = m_Mesh.transform; - int len = m_CutPath.Count; - - Event evt = Event.current; - - if (evt.type == EventType.Repaint) - { - if (!m_CurrentPosition.Equals(Vector3.positiveInfinity)) - { - Vector3 point = trs.TransformPoint(m_CurrentPosition); - if(m_SelectedIndex >= 0 && m_SelectedIndex < m_CutPath.Count) - point = trs.TransformPoint(m_CutPath[m_SelectedIndex].position); - - float size = HandleUtility.GetHandleSize(point) * k_HandleSize; - Handles.color = m_CurrentHandleColor; - Handles.DotHandleCap(-1, point, Quaternion.identity, size, evt.type); - } - Handles.color = Color.white; - } - } - - /// - /// Visual indications to help the user: highlighting faces, edges and vertices when snapping on them - /// - void DoVisualCues() - { - if(m_Mesh != null) - { - if(m_TargetFace == null && m_CurrentFace != null) - EditorHandleDrawing.HighlightFaces(m_Mesh, new Face[]{m_CurrentFace}, Color.Lerp(Color.blue, Color.cyan, 0.5f)); - - EditorHandleDrawing.HighlightVertices(m_Mesh, m_SelectedVertices, false); - EditorHandleDrawing.HighlightEdges(m_Mesh, m_SelectedEdges,false); - - if(m_TargetFace != null) - { - if(m_SnapedVertexId != -1) - EditorHandleDrawing.HighlightVertices(m_Mesh, new int[] { m_SnapedVertexId }); - - if(m_SnapedEdge != Edge.Empty) - EditorHandleDrawing.HighlightEdges(m_Mesh, new Edge[] { m_SnapedEdge }); - } - } - } - - /// - /// Rebuild the line mesh when updated - /// - void RebuildCutShape(bool optimize = true) - { - // If Undo is called immediately after creation this situation can occur - if (m_Mesh == null) - return; - - UpdateMeshConnections(); - ValidateCutShape(); - - // While the vertex count may not change, the triangle winding might. So unfortunately we can't take - // advantage of the `vertexCountChanged = false` optimization here. - ProBuilderEditor.Refresh(); - SceneView.RepaintAll(); - - if(optimize && isALoop && m_IsCutValid) - ExecuteCut(); - - m_Dirty = false; - } - - void ValidateCutShape() - { - Vector3[] verticesPositions = m_Mesh.positionsInternal; - - m_IsCutValid = true; - - //For all segments of the current cut - for(int i = 0; i < m_CutPath.Count-1 && m_IsCutValid; i++) - { - Vector2 segment1Start2D = HandleUtility.WorldToGUIPoint(m_Mesh.transform.TransformPoint(m_CutPath[i].position)); - Vector2 segment1End2D = HandleUtility.WorldToGUIPoint(m_Mesh.transform.TransformPoint(m_CutPath[i+1].position)); - - int lastVertexIndex = (isALoop && i == 0) ? m_CutPath.Count-2 : m_CutPath.Count-1; - //Test intersections with the rest of the cut path - for(int j = i + 2; j < lastVertexIndex && m_IsCutValid; j++) - { - if(((m_CutPath[j].types | m_CutPath[j+1].types) & VertexTypes.VertexInShape) == 0) - { - Vector2 segment2Start2D = - HandleUtility.WorldToGUIPoint(m_Mesh.transform.TransformPoint(m_CutPath[j].position)); - Vector2 segment2End2D = - HandleUtility.WorldToGUIPoint(m_Mesh.transform.TransformPoint(m_CutPath[j + 1].position)); - - m_IsCutValid = !Math.GetLineSegmentIntersect(segment1Start2D, segment1End2D, segment2Start2D, - segment2End2D); - } - } - - if(( (m_CutPath[i].types| m_CutPath[i+1].types) & VertexTypes.VertexInShape ) == 0) - { - //Test intersections with the connections to the face vertices - for(int j = 0; j < m_MeshConnections.Count && m_IsCutValid; j++) - { - SimpleTuple connection = m_MeshConnections[j]; - - if(connection.item1 != i && connection.item1 != i + 1) - { - Vector2 segment2Start2D = - HandleUtility.WorldToGUIPoint( - m_Mesh.transform.TransformPoint(m_CutPath[connection.item1].position)); - Vector2 segment2End2D = - HandleUtility.WorldToGUIPoint( - m_Mesh.transform.TransformPoint(verticesPositions[connection.item2])); - - m_IsCutValid = !Math.GetLineSegmentIntersect(segment1Start2D, segment1End2D, - segment2Start2D, - segment2End2D); - } - } - } - } - - //For all connections to the face vertices - for(int i = 0; i < m_MeshConnections.Count-1 && m_IsCutValid; i++) - { - SimpleTuple connection1 = m_MeshConnections[i]; - Vector2 segment1Start2D = - HandleUtility.WorldToGUIPoint( - m_Mesh.transform.TransformPoint(m_CutPath[connection1.item1].position) ); - Vector2 segment1End2D = - HandleUtility.WorldToGUIPoint( - m_Mesh.transform.TransformPoint(verticesPositions[connection1.item2])); - - //Test intersection with the other connections to the face vertices - for(int j = i+1; j < m_MeshConnections.Count && m_IsCutValid; j++) - { - SimpleTuple connection2 = m_MeshConnections[j]; - - Vector2 segment2Start2D = - HandleUtility.WorldToGUIPoint( - m_Mesh.transform.TransformPoint(m_CutPath[connection2.item1].position)); - Vector2 segment2End2D = - HandleUtility.WorldToGUIPoint( - m_Mesh.transform.TransformPoint(verticesPositions[connection2.item2])); - - m_IsCutValid = !Math.GetLineSegmentIntersect(segment1Start2D, segment1End2D, segment2Start2D, segment2End2D); - } - } - - } - - /// - /// Display lines of the cut shape - /// - void DoExistingLinesGUI() - { - DrawCutLine(); - DrawMeshConnectionsHandles(); - } - - - /// - /// Draw the line corresponding to the current cut path in the face - /// - void DrawCutLine() - { - Handles.color = m_IsCutValid ? k_LineColor : k_InvalidLineColor; - Handles.DrawPolyLine(m_CutPath.Select(tup => m_Mesh.transform.TransformPoint(tup.position)).ToArray()); - Handles.color = Color.white; - } - - /// - /// Draw a helper line between the last point of the cut and the current position of the mouse cursor - /// - void DrawGuideLine() - { - if(m_CurrentPosition.Equals(Vector3.positiveInfinity) || m_ModifyingPoint) - return ; - - if(m_CutPath.Count > 0) - { - Handles.color = k_DrawingLineColor; - Handles.DrawDottedLine(m_Mesh.transform.TransformPoint(m_CutPath[m_CutPath.Count - 1].position), - m_Mesh.transform.TransformPoint(m_CurrentPosition), 5f); - Handles.color = Color.white; - } - } - - /// - /// Draw a helper line to show which vertices of the face are connected to points of the cut shape - /// - void DrawMeshConnectionsHandles() - { - if(m_MeshConnections.Count > 0 && m_Mesh != null) - { - Vertex[] vertices = m_Mesh.GetVertices(); - for (int i = m_MeshConnections.Count - 1; i >= 0; i--) - { - var connection = m_MeshConnections[i]; - if (connection.item1 < 0 || connection.item1 >= m_CutPath.Count - || connection.item2 < 0 || connection.item2 >= vertices.Length) - { - m_MeshConnections.RemoveAt(i); - continue; - } - Handles.color = k_ConnectionsLineColor; - Handles.DrawDottedLine(m_Mesh.transform.TransformPoint(m_CutPath[connection.item1].position), - m_Mesh.transform.TransformPoint(vertices[connection.item2].position), 5f); - Handles.color = Color.white; - } - } - } - } } From 418dcf331344ee15f53d8f1e0b2cdfa619933d5f Mon Sep 17 00:00:00 2001 From: Sachin Date: Mon, 25 May 2026 22:39:12 +0530 Subject: [PATCH 07/13] fix cut tool missing face --- Editor/EditorCore/CutTool.Geometry.cs | 3 +-- Editor/EditorCore/CutTool.Rectangle.cs | 9 +-------- Editor/EditorCore/CutTool.UI.cs | 2 +- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/Editor/EditorCore/CutTool.Geometry.cs b/Editor/EditorCore/CutTool.Geometry.cs index 2b09e03b6..3ec5778e3 100644 --- a/Editor/EditorCore/CutTool.Geometry.cs +++ b/Editor/EditorCore/CutTool.Geometry.cs @@ -91,8 +91,7 @@ internal ActionResult DoCut() //Compute the rest of the new faces (faces outside of the loop or division of the original face) List faces = ComputeNewFaces(m_TargetFace, cutIndexes); - if(!isALoop) - newFaces.AddRange(faces); + newFaces.AddRange(faces); //Remove inserted vertices only if they were inserted for the process List verticesIndexesToDelete = new List(); diff --git a/Editor/EditorCore/CutTool.Rectangle.cs b/Editor/EditorCore/CutTool.Rectangle.cs index 339eb5edf..97d2481d9 100644 --- a/Editor/EditorCore/CutTool.Rectangle.cs +++ b/Editor/EditorCore/CutTool.Rectangle.cs @@ -153,15 +153,8 @@ void DoRectanglePlacement(EditorWindow window) m_CurrentFace = m_TargetFace; AddCurrentPositionToPath(false); - // RebuildCutShape(true) already executes the cut if the path is valid - // Don't call DoCut() again — it would run on already-cleared data + // Don't auto-execute—let user click Complete button like point mode RebuildCutShape(false); - - if (m_CutPath.Count >= 2 && m_IsCutValid) - { - ActionResult result = DoCut(); - EditorUtility.ShowNotification(result.notification); - } } m_RectStartPoint = Vector3.positiveInfinity; diff --git a/Editor/EditorCore/CutTool.UI.cs b/Editor/EditorCore/CutTool.UI.cs index 2b2053b0a..2dd4b3386 100644 --- a/Editor/EditorCore/CutTool.UI.cs +++ b/Editor/EditorCore/CutTool.UI.cs @@ -73,7 +73,7 @@ void OnOverlayGUI(UObject target, SceneView view) } else { - if(!m_RectangleMode && m_CutPath.Count > 1) + if(m_CutPath.Count > 1 && m_IsCutValid) { if(GUILayout.Button(EditorGUIUtility.TrTextContent("Complete"))) ExecuteCut(); From 7df978a65ffa48a26b3c8c4d516492525076fe52 Mon Sep 17 00:00:00 2001 From: Sachin Date: Mon, 25 May 2026 23:07:37 +0530 Subject: [PATCH 08/13] Optimize cut tool performance by reducing expensive WorldToGUIPoint calls and improving vertex lookups --- Editor/EditorCore/CutTool.Geometry.cs | 29 +++++++++++++---- Editor/EditorCore/CutTool.Rectangle.cs | 41 +++++++++++------------ Editor/EditorCore/CutTool.Snapping.cs | 36 ++++++++++----------- Editor/EditorCore/CutTool.Validation.cs | 43 +++++++++++-------------- 4 files changed, 80 insertions(+), 69 deletions(-) diff --git a/Editor/EditorCore/CutTool.Geometry.cs b/Editor/EditorCore/CutTool.Geometry.cs index 3ec5778e3..74a0d9abd 100644 --- a/Editor/EditorCore/CutTool.Geometry.cs +++ b/Editor/EditorCore/CutTool.Geometry.cs @@ -57,15 +57,21 @@ internal ActionResult DoCut() //Insert cut vertices in the mesh List cutVertices = InsertVertices(); m_Mesh.GetVerticesInList(meshVertices); + + // Build vertex→index dictionary for O(1) lookups instead of O(N) IndexOf + Dictionary vertexIndexMap = new Dictionary(); + for (int i = 0; i < meshVertices.Count; i++) + vertexIndexMap[meshVertices[i]] = i; + //Retrieve indexes of the cut points in the mesh vertices - int[] cutIndexes = cutVertices.Select(vert => meshVertices.IndexOf(vert)).ToArray(); + int[] cutIndexes = cutVertices.Select(vert => vertexIndexMap[vert]).ToArray(); //Update mesh connections with new indexes for(int i = 0; i connection = m_MeshConnections[i]; - connection.item1 = meshVertices.IndexOf(cutVertices[connection.item1]); - connection.item2 = meshVertices.IndexOf(formerVertices[i]); + connection.item1 = vertexIndexMap[cutVertices[connection.item1]]; + connection.item2 = vertexIndexMap[formerVertices[i]]; m_MeshConnections[i] = connection; } @@ -111,8 +117,17 @@ internal ActionResult DoCut() m_Mesh.Optimize(); //Update mesh selection after the cut has been performed + // For loop cuts, select only the central cutout so user can immediately manipulate it MeshSelection.ClearElementSelection(); - m_Mesh.SetSelectedFaces(newFaces); + if (isALoop) + { + // Select only the central loop face (first in the list) + m_Mesh.SetSelectedFaces(new Face[] { newFaces[0] }); + } + else + { + m_Mesh.SetSelectedFaces(newFaces); + } ProBuilderEditor.Refresh(); ResetToolState(true); @@ -434,7 +449,7 @@ Vertex InsertVertexOnExistingVertex(Vector3 vertexPosition) /// The inew vertex inserted Vertex InsertVertexOnExistingEdge(Vector3 vertexPosition) { - List vertices = m_Mesh.GetVertices().ToList(); + Vector3[] vertexPositions = m_Mesh.positionsInternal; List peripheralEdges = WingedEdge.SortEdgesByAdjacency(m_TargetFace); int bestIndex = -1; @@ -442,8 +457,8 @@ Vertex InsertVertexOnExistingEdge(Vector3 vertexPosition) for (int i = 0; i < peripheralEdges.Count; i++) { float dist = UnityEngine.ProBuilder.Math.DistancePointLineSegment(vertexPosition, - vertices[peripheralEdges[i].a].position, - vertices[peripheralEdges[i].b].position); + vertexPositions[peripheralEdges[i].a], + vertexPositions[peripheralEdges[i].b]); if (dist < bestDistance) { diff --git a/Editor/EditorCore/CutTool.Rectangle.cs b/Editor/EditorCore/CutTool.Rectangle.cs index 97d2481d9..632089b82 100644 --- a/Editor/EditorCore/CutTool.Rectangle.cs +++ b/Editor/EditorCore/CutTool.Rectangle.cs @@ -20,6 +20,18 @@ namespace UnityEditor.ProBuilder { partial class CutTool { + /// + /// Create a local coordinate system on the face plane. + /// + static void GetFacePlaneAxes(Vector3 faceNormal, out Vector3 faceRight, out Vector3 faceUp) + { + if (Mathf.Abs(Vector3.Dot(faceNormal, Vector3.up)) > 0.99f) + faceRight = Vector3.Cross(faceNormal, Vector3.forward).normalized; + else + faceRight = Vector3.Cross(faceNormal, Vector3.up).normalized; + faceUp = Vector3.Cross(faceNormal, faceRight).normalized; + } + /// /// Rectangle mode: click and drag to define a rectangular cut on the face. /// On mouse up, auto-places the 4 corners and executes the cut. @@ -92,20 +104,15 @@ void DoRectanglePlacement(EditorWindow window) // Compute face normal for projection Vector3 faceNormal = Math.Normal(m_Mesh, m_TargetFace); - // Create a local coordinate system on the face plane Vector3 faceRight, faceUp; - if (Mathf.Abs(Vector3.Dot(faceNormal, Vector3.up)) > 0.99f) - faceRight = Vector3.Cross(faceNormal, Vector3.forward).normalized; - else - faceRight = Vector3.Cross(faceNormal, Vector3.up).normalized; - faceUp = Vector3.Cross(faceNormal, faceRight).normalized; + GetFacePlaneAxes(faceNormal, out faceRight, out faceUp); // Decompose rect diagonals in face space Vector3 diagonal = end - start; float rightDot = Vector3.Dot(diagonal, faceRight); float upDot = Vector3.Dot(diagonal, faceUp); - // Compute the 4 rectangle corners in world space + // Compute the 4 rectangle corners in local space Vector3 corner0 = start; Vector3 corner1 = start + faceRight * rightDot; Vector3 corner2 = end; @@ -180,27 +187,21 @@ void DoRectanglePreview() return; Transform trs = m_Mesh.transform; - Vector3 startW = trs.TransformPoint(m_RectStartPoint); - Vector3 endW = trs.TransformPoint(m_RectEndPoint); - // Compute face plane + // Compute the 4 corners in local space Vector3 faceNormal = Math.Normal(m_Mesh, m_TargetFace); Vector3 faceRight, faceUp; - if (Mathf.Abs(Vector3.Dot(faceNormal, Vector3.up)) > 0.99f) - faceRight = Vector3.Cross(faceNormal, Vector3.forward).normalized; - else - faceRight = Vector3.Cross(faceNormal, Vector3.up).normalized; - faceUp = Vector3.Cross(faceNormal, faceRight).normalized; + GetFacePlaneAxes(faceNormal, out faceRight, out faceUp); - Vector3 diagonal = endW - startW; + Vector3 diagonal = m_RectEndPoint - m_RectStartPoint; float rightDot = Vector3.Dot(diagonal, faceRight); float upDot = Vector3.Dot(diagonal, faceUp); - Vector3 c0 = startW; - Vector3 c1 = startW + faceRight * rightDot; - Vector3 c2 = endW; - Vector3 c3 = startW + faceUp * upDot; + Vector3 c0 = trs.TransformPoint(m_RectStartPoint); + Vector3 c1 = trs.TransformPoint(m_RectStartPoint + faceRight * rightDot); + Vector3 c2 = trs.TransformPoint(m_RectEndPoint); + Vector3 c3 = trs.TransformPoint(m_RectStartPoint + faceUp * upDot); // Draw filled rectangle Handles.color = k_RectPreviewColor; diff --git a/Editor/EditorCore/CutTool.Snapping.cs b/Editor/EditorCore/CutTool.Snapping.cs index 9664fd18c..da5fd5308 100644 --- a/Editor/EditorCore/CutTool.Snapping.cs +++ b/Editor/EditorCore/CutTool.Snapping.cs @@ -74,7 +74,7 @@ internal void UpdateMeshConnections() return; List existingVerticesInCut = - m_CutPath.Where(v => ( v.types | VertexTypes.ExistingVertex ) != 0) + m_CutPath.Where(v => ( v.types & VertexTypes.ExistingVertex ) != 0) .Select(v => v.position).ToList(); Vector3[] verticesPositions = m_Mesh.positionsInternal; @@ -247,7 +247,7 @@ void CheckPointInMesh() m_SnapedVertexId = -1; m_SnapedEdge = Edge.Empty; - Vertex[] vertices = m_Mesh.GetVertices(); + Vector3[] vertexPositions = m_Mesh.positionsInternal; List peripheralEdges = WingedEdge.SortEdgesByAdjacency(m_CurrentFace); if (m_TargetFace != null && m_CurrentFace != m_TargetFace) peripheralEdges = WingedEdge.SortEdgesByAdjacency(m_TargetFace); @@ -255,7 +255,7 @@ void CheckPointInMesh() { if ((m_TargetFace == null || m_TargetFace == m_CurrentFace) && m_SnappingPoint) { - if (Math.Approx3(vertices[peripheralEdges[i].a].position, + if (Math.Approx3(vertexPositions[peripheralEdges[i].a], m_CurrentPosition, snapDistance)) { @@ -267,8 +267,8 @@ void CheckPointInMesh() { float dist = Math.DistancePointLineSegment( m_CurrentPosition, - vertices[peripheralEdges[i].a].position, - vertices[peripheralEdges[i].b].position); + vertexPositions[peripheralEdges[i].a], + vertexPositions[peripheralEdges[i].b]); if (dist < Mathf.Min(snapDistance, bestDistance)) { @@ -280,7 +280,7 @@ void CheckPointInMesh() //Even with no snapping, try to detect if the first point is on a existing geometry else if(m_TargetFace == null && !m_SnappingPoint) { - if (Math.Approx3(vertices[peripheralEdges[i].a].position, + if (Math.Approx3(vertexPositions[peripheralEdges[i].a], m_CurrentPosition, 0.01f)) { @@ -292,8 +292,8 @@ void CheckPointInMesh() { float dist = Math.DistancePointLineSegment( m_CurrentPosition, - vertices[peripheralEdges[i].a].position, - vertices[peripheralEdges[i].b].position); + vertexPositions[peripheralEdges[i].a], + vertexPositions[peripheralEdges[i].b]); if (dist < Mathf.Min(0.01f, bestDistance)) { @@ -305,11 +305,11 @@ void CheckPointInMesh() else if(m_CurrentFace != m_TargetFace && m_TargetFace != null ) { float edgeDist = Math.DistancePointLineSegment(m_CurrentPosition, - vertices[peripheralEdges[i].a].position, - vertices[peripheralEdges[i].b].position); + vertexPositions[peripheralEdges[i].a], + vertexPositions[peripheralEdges[i].b]); float vertexDist = Vector3.Distance(m_CurrentPosition, - vertices[peripheralEdges[i].a].position); + vertexPositions[peripheralEdges[i].a]); if (edgeDist < vertexDist && edgeDist < bestDistance) { @@ -330,7 +330,7 @@ void CheckPointInMesh() //We found a close vertex if (snapedOnVertex) { - m_CurrentPosition = vertices[peripheralEdges[bestIndex].a].position; + m_CurrentPosition = vertexPositions[peripheralEdges[bestIndex].a]; m_CurrentVertexTypes = VertexTypes.ExistingVertex; m_SelectedIndex = -1; @@ -342,8 +342,8 @@ void CheckPointInMesh() { if (m_TargetFace == null || m_TargetFace == m_CurrentFace) { - Vector3 left = vertices[peripheralEdges[bestIndex].a].position, - right = vertices[peripheralEdges[bestIndex].b].position; + Vector3 left = vertexPositions[peripheralEdges[bestIndex].a], + right = vertexPositions[peripheralEdges[bestIndex].b]; float x = (m_CurrentPosition - left).magnitude; float y = (m_CurrentPosition - right).magnitude; @@ -353,13 +353,13 @@ void CheckPointInMesh() else //if(m_CurrentFace != m_TargetFace) { Vector3 a = m_CurrentPosition - - vertices[peripheralEdges[bestIndex].a].position; - Vector3 b = vertices[peripheralEdges[bestIndex].b].position - - vertices[peripheralEdges[bestIndex].a].position; + vertexPositions[peripheralEdges[bestIndex].a]; + Vector3 b = vertexPositions[peripheralEdges[bestIndex].b] - + vertexPositions[peripheralEdges[bestIndex].a]; float angle = Vector3.Angle(b, a); m_CurrentPosition = Vector3.Magnitude(a) * Mathf.Cos(angle * Mathf.Deg2Rad) * b / Vector3.Magnitude(b); - m_CurrentPosition += vertices[peripheralEdges[bestIndex].a].position; + m_CurrentPosition += vertexPositions[peripheralEdges[bestIndex].a]; } m_SnapedEdge = peripheralEdges[bestIndex]; diff --git a/Editor/EditorCore/CutTool.Validation.cs b/Editor/EditorCore/CutTool.Validation.cs index 562424c86..34b62bfee 100644 --- a/Editor/EditorCore/CutTool.Validation.cs +++ b/Editor/EditorCore/CutTool.Validation.cs @@ -37,11 +37,20 @@ void ValidateCutShape() m_IsCutValid = true; + // Pre-calculate all 2D screen positions to avoid expensive WorldToGUIPoint calls in nested loops + Transform trs = m_Mesh.transform; + Vector2[] cutPath2D = new Vector2[m_CutPath.Count]; + Vector2[] meshConnection2D = new Vector2[m_MeshConnections.Count]; + for (int i = 0; i < m_CutPath.Count; i++) + cutPath2D[i] = HandleUtility.WorldToGUIPoint(trs.TransformPoint(m_CutPath[i].position)); + for (int i = 0; i < m_MeshConnections.Count; i++) + meshConnection2D[i] = HandleUtility.WorldToGUIPoint(trs.TransformPoint(verticesPositions[m_MeshConnections[i].item2])); + //For all segments of the current cut for(int i = 0; i < m_CutPath.Count-1 && m_IsCutValid; i++) { - Vector2 segment1Start2D = HandleUtility.WorldToGUIPoint(m_Mesh.transform.TransformPoint(m_CutPath[i].position)); - Vector2 segment1End2D = HandleUtility.WorldToGUIPoint(m_Mesh.transform.TransformPoint(m_CutPath[i+1].position)); + Vector2 segment1Start2D = cutPath2D[i]; + Vector2 segment1End2D = cutPath2D[i + 1]; int lastVertexIndex = (isALoop && i == 0) ? m_CutPath.Count-2 : m_CutPath.Count-1; //Test intersections with the rest of the cut path @@ -49,10 +58,8 @@ void ValidateCutShape() { if(((m_CutPath[j].types | m_CutPath[j+1].types) & VertexTypes.VertexInShape) == 0) { - Vector2 segment2Start2D = - HandleUtility.WorldToGUIPoint(m_Mesh.transform.TransformPoint(m_CutPath[j].position)); - Vector2 segment2End2D = - HandleUtility.WorldToGUIPoint(m_Mesh.transform.TransformPoint(m_CutPath[j + 1].position)); + Vector2 segment2Start2D = cutPath2D[j]; + Vector2 segment2End2D = cutPath2D[j + 1]; m_IsCutValid = !Math.GetLineSegmentIntersect(segment1Start2D, segment1End2D, segment2Start2D, segment2End2D); @@ -68,12 +75,8 @@ void ValidateCutShape() if(connection.item1 != i && connection.item1 != i + 1) { - Vector2 segment2Start2D = - HandleUtility.WorldToGUIPoint( - m_Mesh.transform.TransformPoint(m_CutPath[connection.item1].position)); - Vector2 segment2End2D = - HandleUtility.WorldToGUIPoint( - m_Mesh.transform.TransformPoint(verticesPositions[connection.item2])); + Vector2 segment2Start2D = cutPath2D[connection.item1]; + Vector2 segment2End2D = meshConnection2D[j]; m_IsCutValid = !Math.GetLineSegmentIntersect(segment1Start2D, segment1End2D, segment2Start2D, @@ -87,24 +90,16 @@ void ValidateCutShape() for(int i = 0; i < m_MeshConnections.Count-1 && m_IsCutValid; i++) { SimpleTuple connection1 = m_MeshConnections[i]; - Vector2 segment1Start2D = - HandleUtility.WorldToGUIPoint( - m_Mesh.transform.TransformPoint(m_CutPath[connection1.item1].position) ); - Vector2 segment1End2D = - HandleUtility.WorldToGUIPoint( - m_Mesh.transform.TransformPoint(verticesPositions[connection1.item2])); + Vector2 segment1Start2D = cutPath2D[connection1.item1]; + Vector2 segment1End2D = meshConnection2D[i]; //Test intersection with the other connections to the face vertices for(int j = i+1; j < m_MeshConnections.Count && m_IsCutValid; j++) { SimpleTuple connection2 = m_MeshConnections[j]; - Vector2 segment2Start2D = - HandleUtility.WorldToGUIPoint( - m_Mesh.transform.TransformPoint(m_CutPath[connection2.item1].position)); - Vector2 segment2End2D = - HandleUtility.WorldToGUIPoint( - m_Mesh.transform.TransformPoint(verticesPositions[connection2.item2])); + Vector2 segment2Start2D = cutPath2D[connection2.item1]; + Vector2 segment2End2D = meshConnection2D[j]; m_IsCutValid = !Math.GetLineSegmentIntersect(segment1Start2D, segment1End2D, segment2Start2D, segment2End2D); } From 472f697cf6c6af85761fe8a406c0907f29f4155e Mon Sep 17 00:00:00 2001 From: Sachin Date: Tue, 26 May 2026 00:35:50 +0530 Subject: [PATCH 09/13] allow cut vertical holes --- Editor/EditorCore/CutTool.Validation.cs | 28 ++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/Editor/EditorCore/CutTool.Validation.cs b/Editor/EditorCore/CutTool.Validation.cs index 34b62bfee..adb006d4b 100644 --- a/Editor/EditorCore/CutTool.Validation.cs +++ b/Editor/EditorCore/CutTool.Validation.cs @@ -37,14 +37,32 @@ void ValidateCutShape() m_IsCutValid = true; - // Pre-calculate all 2D screen positions to avoid expensive WorldToGUIPoint calls in nested loops - Transform trs = m_Mesh.transform; + // Project all points onto the face plane (2D) for view-independent intersection testing. + // Using screen-space projection causes false positives for faces viewed at an angle (e.g. floors). + Vector3 faceNormal = m_TargetFace != null + ? Math.Normal(m_Mesh, m_TargetFace) + : Vector3.up; + + Vector3 faceRight, faceUp; + if (Mathf.Abs(Vector3.Dot(faceNormal, Vector3.up)) > 0.99f) + faceRight = Vector3.Cross(faceNormal, Vector3.forward).normalized; + else + faceRight = Vector3.Cross(faceNormal, Vector3.up).normalized; + faceUp = Vector3.Cross(faceNormal, faceRight).normalized; + Vector2[] cutPath2D = new Vector2[m_CutPath.Count]; - Vector2[] meshConnection2D = new Vector2[m_MeshConnections.Count]; for (int i = 0; i < m_CutPath.Count; i++) - cutPath2D[i] = HandleUtility.WorldToGUIPoint(trs.TransformPoint(m_CutPath[i].position)); + { + Vector3 p = m_CutPath[i].position; + cutPath2D[i] = new Vector2(Vector3.Dot(p, faceRight), Vector3.Dot(p, faceUp)); + } + + Vector2[] meshConnection2D = new Vector2[m_MeshConnections.Count]; for (int i = 0; i < m_MeshConnections.Count; i++) - meshConnection2D[i] = HandleUtility.WorldToGUIPoint(trs.TransformPoint(verticesPositions[m_MeshConnections[i].item2])); + { + Vector3 p = verticesPositions[m_MeshConnections[i].item2]; + meshConnection2D[i] = new Vector2(Vector3.Dot(p, faceRight), Vector3.Dot(p, faceUp)); + } //For all segments of the current cut for(int i = 0; i < m_CutPath.Count-1 && m_IsCutValid; i++) From 7a2a64f060039bddd520be18003d7dc46e2c7443 Mon Sep 17 00:00:00 2001 From: Sachin Date: Tue, 26 May 2026 01:48:25 +0530 Subject: [PATCH 10/13] Refactor cut tool geometry calculations for improved clarity and performance --- Editor/EditorCore/CutTool.Geometry.cs | 13 +++---- Editor/EditorCore/CutTool.Rectangle.cs | 49 +++++++++++++++++--------- Editor/EditorCore/CutTool.UI.cs | 14 +++++--- 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/Editor/EditorCore/CutTool.Geometry.cs b/Editor/EditorCore/CutTool.Geometry.cs index 74a0d9abd..934594960 100644 --- a/Editor/EditorCore/CutTool.Geometry.cs +++ b/Editor/EditorCore/CutTool.Geometry.cs @@ -249,13 +249,10 @@ Face ComputeFaceClosure( List polygonStart, int currentIndex, List cut int polygonFirstSharedIndex = sharedToUnique[polygonFirstVertex]; - bool hasConnection = m_MeshConnections.Exists(tup => sharedToUnique.ContainsKey(tup.item2) - && sharedToUnique[tup.item2] == polygonFirstSharedIndex); - SimpleTuple connection = default; - - if (hasConnection) - connection = m_MeshConnections.Find(tup => sharedToUnique.ContainsKey(tup.item2) - && sharedToUnique[tup.item2] == polygonFirstSharedIndex); + int connectionIndex = m_MeshConnections.FindIndex(tup => + sharedToUnique.ContainsKey(tup.item2) && sharedToUnique[tup.item2] == polygonFirstSharedIndex); + bool hasConnection = connectionIndex >= 0; + SimpleTuple connection = hasConnection ? m_MeshConnections[connectionIndex] : default; List> closureCandidates = new List>(); @@ -366,7 +363,7 @@ bool TryGetFaceArea(Face face, List meshVertices, IList un if (face == null || face.indexesInternal == null) return false; - Vector3[] vertices = meshVertices.Select(vertex => vertex.position).ToArray(); + Vector3[] vertices = m_Mesh.positionsInternal; int[] indexes = new int[face.indexesInternal.Length]; for (int i = 0; i < face.indexesInternal.Length; i++) diff --git a/Editor/EditorCore/CutTool.Rectangle.cs b/Editor/EditorCore/CutTool.Rectangle.cs index 632089b82..e3b2aff43 100644 --- a/Editor/EditorCore/CutTool.Rectangle.cs +++ b/Editor/EditorCore/CutTool.Rectangle.cs @@ -112,19 +112,19 @@ void DoRectanglePlacement(EditorWindow window) float rightDot = Vector3.Dot(diagonal, faceRight); float upDot = Vector3.Dot(diagonal, faceUp); - // Compute the 4 rectangle corners in local space + // Compute all 4 corners strictly on the face plane (coplanar) Vector3 corner0 = start; Vector3 corner1 = start + faceRight * rightDot; - Vector3 corner2 = end; + Vector3 corner2 = start + faceRight * rightDot + faceUp * upDot; Vector3 corner3 = start + faceUp * upDot; - // Snap all corners to grid if enabled + // Snap only start point to grid; recompute derived corners to stay on-plane if (m_SnapToGrid) { corner0 = ProBuilderSnapping.Snap(corner0, EditorSnapping.activeMoveSnapValue); - corner1 = ProBuilderSnapping.Snap(corner1, EditorSnapping.activeMoveSnapValue); - corner2 = ProBuilderSnapping.Snap(corner2, EditorSnapping.activeMoveSnapValue); - corner3 = ProBuilderSnapping.Snap(corner3, EditorSnapping.activeMoveSnapValue); + corner1 = corner0 + faceRight * rightDot; + corner2 = corner0 + faceRight * rightDot + faceUp * upDot; + corner3 = corner0 + faceUp * upDot; } if (HasSignificantRectangle(corner0, corner2)) @@ -132,32 +132,49 @@ void DoRectanglePlacement(EditorWindow window) // Build cut path: 4 corners + close back to start to form a loop UndoUtility.RecordObject(this, "Rectangle Cut"); - m_CurrentPosition = corner0; m_CurrentPositionNormal = faceNormal; - m_CurrentVertexTypes = VertexTypes.NewVertex; m_CurrentFace = m_TargetFace; + + // Corner 0: run geometry snap so it connects to existing vertices/edges + m_CurrentPosition = corner0; + m_CurrentVertexTypes = VertexTypes.None; + if (m_SnapToGeometry) + CheckPointInMesh(); + if (m_CurrentVertexTypes == VertexTypes.None) + m_CurrentVertexTypes = VertexTypes.NewVertex; + corner0 = m_CurrentPosition; AddCurrentPositionToPath(false); + // Corner 1 m_CurrentPosition = corner1; - m_CurrentPositionNormal = faceNormal; - m_CurrentVertexTypes = VertexTypes.NewVertex; + m_CurrentVertexTypes = VertexTypes.None; + if (m_SnapToGeometry) + CheckPointInMesh(); + if (m_CurrentVertexTypes == VertexTypes.None) + m_CurrentVertexTypes = VertexTypes.NewVertex; AddCurrentPositionToPath(false); + // Corner 2 m_CurrentPosition = corner2; - m_CurrentPositionNormal = faceNormal; - m_CurrentVertexTypes = VertexTypes.NewVertex; + m_CurrentVertexTypes = VertexTypes.None; + if (m_SnapToGeometry) + CheckPointInMesh(); + if (m_CurrentVertexTypes == VertexTypes.None) + m_CurrentVertexTypes = VertexTypes.NewVertex; AddCurrentPositionToPath(false); + // Corner 3 m_CurrentPosition = corner3; - m_CurrentPositionNormal = faceNormal; - m_CurrentVertexTypes = VertexTypes.NewVertex; + m_CurrentVertexTypes = VertexTypes.None; + if (m_SnapToGeometry) + CheckPointInMesh(); + if (m_CurrentVertexTypes == VertexTypes.None) + m_CurrentVertexTypes = VertexTypes.NewVertex; AddCurrentPositionToPath(false); // Close the loop by returning to the start corner m_CurrentPosition = corner0; - m_CurrentPositionNormal = faceNormal; m_CurrentVertexTypes = VertexTypes.VertexInShape; - m_CurrentFace = m_TargetFace; AddCurrentPositionToPath(false); // Don't auto-execute—let user click Complete button like point mode diff --git a/Editor/EditorCore/CutTool.UI.cs b/Editor/EditorCore/CutTool.UI.cs index 2dd4b3386..7f2f7a088 100644 --- a/Editor/EditorCore/CutTool.UI.cs +++ b/Editor/EditorCore/CutTool.UI.cs @@ -193,7 +193,10 @@ void DoExistingLinesGUI() void DrawCutLine() { Handles.color = m_IsCutValid ? k_LineColor : k_InvalidLineColor; - Handles.DrawPolyLine(m_CutPath.Select(tup => m_Mesh.transform.TransformPoint(tup.position)).ToArray()); + Transform trs = m_Mesh.transform; + for (int i = 0; i < m_CutPath.Count - 1; i++) + Handles.DrawLine(trs.TransformPoint(m_CutPath[i].position), + trs.TransformPoint(m_CutPath[i + 1].position)); Handles.color = Color.white; } @@ -221,19 +224,20 @@ void DrawMeshConnectionsHandles() { if(m_MeshConnections.Count > 0 && m_Mesh != null) { - Vertex[] vertices = m_Mesh.GetVertices(); + Vector3[] pos = m_Mesh.positionsInternal; + Transform trs = m_Mesh.transform; for (int i = m_MeshConnections.Count - 1; i >= 0; i--) { var connection = m_MeshConnections[i]; if (connection.item1 < 0 || connection.item1 >= m_CutPath.Count - || connection.item2 < 0 || connection.item2 >= vertices.Length) + || connection.item2 < 0 || connection.item2 >= pos.Length) { m_MeshConnections.RemoveAt(i); continue; } Handles.color = k_ConnectionsLineColor; - Handles.DrawDottedLine(m_Mesh.transform.TransformPoint(m_CutPath[connection.item1].position), - m_Mesh.transform.TransformPoint(vertices[connection.item2].position), 5f); + Handles.DrawDottedLine(trs.TransformPoint(m_CutPath[connection.item1].position), + trs.TransformPoint(pos[connection.item2]), 5f); Handles.color = Color.white; } } From 0ad23829ebbb8322b518b32b2cf384b483f08989 Mon Sep 17 00:00:00 2001 From: Sachin Date: Tue, 26 May 2026 01:56:35 +0530 Subject: [PATCH 11/13] Refactor cut tool geometry and snapping logic for improved performance and clarity --- Editor/EditorCore/CutTool.Geometry.cs | 7 ++----- Editor/EditorCore/CutTool.Snapping.cs | 4 ++-- Editor/EditorCore/CutTool.UI.cs | 16 ++++++++++------ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Editor/EditorCore/CutTool.Geometry.cs b/Editor/EditorCore/CutTool.Geometry.cs index 934594960..d9d992fd2 100644 --- a/Editor/EditorCore/CutTool.Geometry.cs +++ b/Editor/EditorCore/CutTool.Geometry.cs @@ -232,7 +232,6 @@ List ComputeNewFaces(Face face, IList cutVertexIndexes) /// the valid face that need to be kept in the resulting mesh Face ComputeFaceClosure( List polygonStart, int currentIndex, List cutIndexes, out List facesToDelete) { - List meshVertices = new List(); IList uniqueIdToVertexIndex = m_Mesh.sharedVertices; Dictionary sharedToUnique = m_Mesh.sharedVertexLookup; @@ -307,13 +306,11 @@ Face ComputeFaceClosure( List polygonStart, int currentIndex, List cut closure.AddRange(polygonStart); Face face = m_Mesh.CreatePolygon(closure, false); - meshVertices.Clear(); - m_Mesh.GetVerticesInList(meshVertices); uniqueIdToVertexIndex = m_Mesh.sharedVertices; sharedToUnique = m_Mesh.sharedVertexLookup; float area; - if (!TryGetFaceArea(face, meshVertices, uniqueIdToVertexIndex, sharedToUnique, out area)) + if (!TryGetFaceArea(face, uniqueIdToVertexIndex, sharedToUnique, out area)) { if (face != null) facesToDelete.Add(face); @@ -355,7 +352,7 @@ void ApplySourceFaceSettings(Face destination, Face source) destination.elementGroup = source.elementGroup; } - bool TryGetFaceArea(Face face, List meshVertices, IList uniqueIdToVertexIndex, + bool TryGetFaceArea(Face face, IList uniqueIdToVertexIndex, Dictionary sharedToUnique, out float area) { area = 0f; diff --git a/Editor/EditorCore/CutTool.Snapping.cs b/Editor/EditorCore/CutTool.Snapping.cs index da5fd5308..9b722616a 100644 --- a/Editor/EditorCore/CutTool.Snapping.cs +++ b/Editor/EditorCore/CutTool.Snapping.cs @@ -248,9 +248,9 @@ void CheckPointInMesh() m_SnapedEdge = Edge.Empty; Vector3[] vertexPositions = m_Mesh.positionsInternal; - List peripheralEdges = WingedEdge.SortEdgesByAdjacency(m_CurrentFace); + IList peripheralEdges = m_CurrentFace.edges; if (m_TargetFace != null && m_CurrentFace != m_TargetFace) - peripheralEdges = WingedEdge.SortEdgesByAdjacency(m_TargetFace); + peripheralEdges = m_TargetFace.edges; for (int i = 0; i < peripheralEdges.Count; i++) { if ((m_TargetFace == null || m_TargetFace == m_CurrentFace) && m_SnappingPoint) diff --git a/Editor/EditorCore/CutTool.UI.cs b/Editor/EditorCore/CutTool.UI.cs index 7f2f7a088..4a5de6744 100644 --- a/Editor/EditorCore/CutTool.UI.cs +++ b/Editor/EditorCore/CutTool.UI.cs @@ -26,14 +26,11 @@ void OnOverlayGUI(UObject target, SceneView view) GUI.enabled = MeshSelection.selectedObjectCount == 1; - m_RectangleMode = DoOverlayToggle(L10n.Tr("Rectangle Mode"), m_RectangleMode); - EditorPrefs.SetBool(k_RectangleModePrefKey, m_RectangleMode); + EditorGUI.BeginChangeCheck(); + m_RectangleMode = DoOverlayToggle(L10n.Tr("Rectangle Mode"), m_RectangleMode); m_SnapToGrid = DoOverlayToggle(L10n.Tr("Snap to Grid"), m_SnapToGrid); - EditorPrefs.SetBool(k_SnapToGridPrefKey, m_SnapToGrid); - m_SnapToGeometry = DoOverlayToggle(L10n.Tr("Snap to existing edges and vertices"), m_SnapToGeometry); - EditorPrefs.SetBool(k_SnapToGeometryPrefKey, m_SnapToGeometry); if(m_RectangleMode) { @@ -52,10 +49,17 @@ void OnOverlayGUI(UObject target, SceneView view) { EditorGUILayout.LabelField(L10n.Tr("Snapping distance"), GUILayout.Width(200)); m_SnappingDistance = EditorGUILayout.FloatField(m_SnappingDistance); - EditorPrefs.SetFloat( k_SnappingDistancePrefKey, m_SnappingDistance); } EditorGUI.indentLevel--; + if (EditorGUI.EndChangeCheck()) + { + EditorPrefs.SetBool(k_RectangleModePrefKey, m_RectangleMode); + EditorPrefs.SetBool(k_SnapToGridPrefKey, m_SnapToGrid); + EditorPrefs.SetBool(k_SnapToGeometryPrefKey, m_SnapToGeometry); + EditorPrefs.SetFloat(k_SnappingDistancePrefKey, m_SnappingDistance); + } + GUI.enabled = true; if(MeshSelection.selectedObjectCount != 1) From 8c68e31f4abd9443f1cad3fa7c7b56c3bf5a618a Mon Sep 17 00:00:00 2001 From: Sachin Date: Tue, 26 May 2026 02:06:32 +0530 Subject: [PATCH 12/13] Enhance cut tool snapping logic to project snapped points onto face planes for improved accuracy --- Editor/EditorCore/CutTool.Rectangle.cs | 6 ++++-- Editor/EditorCore/CutTool.Snapping.cs | 13 ++++++++++--- Editor/EditorCore/CutTool.Validation.cs | 6 +----- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Editor/EditorCore/CutTool.Rectangle.cs b/Editor/EditorCore/CutTool.Rectangle.cs index e3b2aff43..9800754e9 100644 --- a/Editor/EditorCore/CutTool.Rectangle.cs +++ b/Editor/EditorCore/CutTool.Rectangle.cs @@ -118,10 +118,12 @@ void DoRectanglePlacement(EditorWindow window) Vector3 corner2 = start + faceRight * rightDot + faceUp * upDot; Vector3 corner3 = start + faceUp * upDot; - // Snap only start point to grid; recompute derived corners to stay on-plane + // Snap only start point to grid, then project back onto face plane if (m_SnapToGrid) { - corner0 = ProBuilderSnapping.Snap(corner0, EditorSnapping.activeMoveSnapValue); + Vector3 snapped = ProBuilderSnapping.Snap(corner0, EditorSnapping.activeMoveSnapValue); + Plane facePlane = new Plane(faceNormal, corner0); + corner0 = facePlane.ClosestPointOnPlane(snapped); corner1 = corner0 + faceRight * rightDot; corner2 = corner0 + faceRight * rightDot + faceUp * upDot; corner3 = corner0 + faceUp * upDot; diff --git a/Editor/EditorCore/CutTool.Snapping.cs b/Editor/EditorCore/CutTool.Snapping.cs index 9b722616a..18fad646c 100644 --- a/Editor/EditorCore/CutTool.Snapping.cs +++ b/Editor/EditorCore/CutTool.Snapping.cs @@ -61,7 +61,11 @@ internal void UpdateCurrentPosition(Face face, Vector3 position, Vector3 normal) void ApplyGridSnap() { if (m_SnapToGrid) - m_CurrentPosition = ProBuilderSnapping.Snap(m_CurrentPosition, EditorSnapping.activeMoveSnapValue); + { + Vector3 snapped = ProBuilderSnapping.Snap(m_CurrentPosition, EditorSnapping.activeMoveSnapValue); + Plane facePlane = new Plane(m_CurrentPositionNormal, m_CurrentPosition); + m_CurrentPosition = facePlane.ClosestPointOnPlane(snapped); + } } /// @@ -141,8 +145,12 @@ internal void UpdateMeshConnections() if(bestVertexIndexToEnd >= 0) m_MeshConnections.Add(new SimpleTuple(m_CutPath.Count - 1,bestVertexIndexToEnd)); } - else if(isALoop && connectionsToBordersCount < 2) + else if(isALoop) { + int requiredConnections = m_RectangleMode ? 4 : 2; + if (connectionsToBordersCount >= requiredConnections) + return; + //The path must have minimum connections with the face borders, find the closest vertices foreach(var vertexIndex in m_TargetFace.distinctIndexes) { @@ -188,7 +196,6 @@ internal void UpdateMeshConnections() (int)Mathf.Sign(Vector3.Distance(m_CutPath[a.item1].position, verticesPositions[a.item2]) - Vector3.Distance(m_CutPath[b.item1].position, verticesPositions[b.item2]))); - int requiredConnections = m_RectangleMode ? 4 : 2; int connectionsCount = Mathf.Max(0, requiredConnections - connectionsToBordersCount); connectionsCount = Mathf.Min(connectionsCount, m_MeshConnections.Count); m_MeshConnections.RemoveRange(connectionsCount,m_MeshConnections.Count - connectionsCount); diff --git a/Editor/EditorCore/CutTool.Validation.cs b/Editor/EditorCore/CutTool.Validation.cs index adb006d4b..90584c395 100644 --- a/Editor/EditorCore/CutTool.Validation.cs +++ b/Editor/EditorCore/CutTool.Validation.cs @@ -44,11 +44,7 @@ void ValidateCutShape() : Vector3.up; Vector3 faceRight, faceUp; - if (Mathf.Abs(Vector3.Dot(faceNormal, Vector3.up)) > 0.99f) - faceRight = Vector3.Cross(faceNormal, Vector3.forward).normalized; - else - faceRight = Vector3.Cross(faceNormal, Vector3.up).normalized; - faceUp = Vector3.Cross(faceNormal, faceRight).normalized; + GetFacePlaneAxes(faceNormal, out faceRight, out faceUp); Vector2[] cutPath2D = new Vector2[m_CutPath.Count]; for (int i = 0; i < m_CutPath.Count; i++) From df2a0eed8c159e10bb47a514e7d00b34bcda9c80 Mon Sep 17 00:00:00 2001 From: Sachin Date: Wed, 27 May 2026 22:12:33 +0530 Subject: [PATCH 13/13] Cut tool optimizations --- Editor/EditorCore/CutTool.Geometry.cs | 17 +++++++---- Editor/EditorCore/CutTool.Rectangle.cs | 15 ++++++++-- Editor/EditorCore/CutTool.Snapping.cs | 41 +++++++++++++++++++++----- Editor/EditorCore/CutTool.cs | 2 ++ 4 files changed, 59 insertions(+), 16 deletions(-) diff --git a/Editor/EditorCore/CutTool.Geometry.cs b/Editor/EditorCore/CutTool.Geometry.cs index d9d992fd2..75b0c5934 100644 --- a/Editor/EditorCore/CutTool.Geometry.cs +++ b/Editor/EditorCore/CutTool.Geometry.cs @@ -58,10 +58,13 @@ internal ActionResult DoCut() List cutVertices = InsertVertices(); m_Mesh.GetVerticesInList(meshVertices); - // Build vertex→index dictionary for O(1) lookups instead of O(N) IndexOf + // Build vertex→index dictionary for O(1) lookups (preserve first-index behavior like IndexOf) Dictionary vertexIndexMap = new Dictionary(); for (int i = 0; i < meshVertices.Count; i++) - vertexIndexMap[meshVertices[i]] = i; + { + if (!vertexIndexMap.ContainsKey(meshVertices[i])) + vertexIndexMap[meshVertices[i]] = i; + } //Retrieve indexes of the cut points in the mesh vertices int[] cutIndexes = cutVertices.Select(vert => vertexIndexMap[vert]).ToArray(); @@ -253,6 +256,10 @@ Face ComputeFaceClosure( List polygonStart, int currentIndex, List cut bool hasConnection = connectionIndex >= 0; SimpleTuple connection = hasConnection ? m_MeshConnections[connectionIndex] : default; + // Hoist loop-invariant dictionary lookups for connection.item1 + int connectionSharedItem1 = -1; + bool hasConnectionSharedItem1 = hasConnection && sharedToUnique.TryGetValue(connection.item1, out connectionSharedItem1); + List> closureCandidates = new List>(); //Go through the cut in reverse direction @@ -265,8 +272,7 @@ Face ComputeFaceClosure( List polygonStart, int currentIndex, List cut int vertexIndex = uniqueIdToVertexIndex[cutIndexes[(index + cutIndexes.Count) % cutIndexes.Count]][0]; candidate.Add(vertexIndex); if(sharedToUnique[vertexIndex] == polygonFirstSharedIndex || - (hasConnection && sharedToUnique.ContainsKey(connection.item1) - && sharedToUnique[vertexIndex] == sharedToUnique[connection.item1])) + (hasConnectionSharedItem1 && sharedToUnique[vertexIndex] == connectionSharedItem1)) { connected = true; break; @@ -286,8 +292,7 @@ Face ComputeFaceClosure( List polygonStart, int currentIndex, List cut int vertexIndex = uniqueIdToVertexIndex[cutIndexes[index % cutIndexes.Count]][0]; candidate.Add(vertexIndex); if(sharedToUnique[vertexIndex] == polygonFirstSharedIndex || - (hasConnection && sharedToUnique.ContainsKey(connection.item1) - && sharedToUnique[vertexIndex] == sharedToUnique[connection.item1])) + (hasConnectionSharedItem1 && sharedToUnique[vertexIndex] == connectionSharedItem1)) { connected = true; break; diff --git a/Editor/EditorCore/CutTool.Rectangle.cs b/Editor/EditorCore/CutTool.Rectangle.cs index 9800754e9..9a9bfccb2 100644 --- a/Editor/EditorCore/CutTool.Rectangle.cs +++ b/Editor/EditorCore/CutTool.Rectangle.cs @@ -222,13 +222,22 @@ void DoRectanglePreview() Vector3 c2 = trs.TransformPoint(m_RectEndPoint); Vector3 c3 = trs.TransformPoint(m_RectStartPoint + faceUp * upDot); - // Draw filled rectangle + // Draw filled rectangle (reuse cached arrays to avoid per-frame allocation) Handles.color = k_RectPreviewColor; - Handles.DrawAAConvexPolygon(new Vector3[] { c0, c1, c2, c3 }); + m_RectConvexPolygon[0] = c0; + m_RectConvexPolygon[1] = c1; + m_RectConvexPolygon[2] = c2; + m_RectConvexPolygon[3] = c3; + Handles.DrawAAConvexPolygon(m_RectConvexPolygon); // Draw outline Handles.color = k_RectOutlineColor; - Handles.DrawAAPolyLine(2f, new Vector3[] { c0, c1, c2, c3, c0 }); + m_RectPreviewPath[0] = c0; + m_RectPreviewPath[1] = c1; + m_RectPreviewPath[2] = c2; + m_RectPreviewPath[3] = c3; + m_RectPreviewPath[4] = c0; + Handles.DrawAAPolyLine(2f, m_RectPreviewPath); } bool TryPassThroughSelection(EditorWindow window, bool hasHitPosition) diff --git a/Editor/EditorCore/CutTool.Snapping.cs b/Editor/EditorCore/CutTool.Snapping.cs index 18fad646c..1b3067198 100644 --- a/Editor/EditorCore/CutTool.Snapping.cs +++ b/Editor/EditorCore/CutTool.Snapping.cs @@ -93,7 +93,16 @@ internal void UpdateMeshConnections() { if(existingVerticesInCut.Count > 0) { - if(existingVerticesInCut.Exists(vert => Math.Approx3(verticesPositions[vertexIndex], vert))) + bool alreadyExists = false; + for (int ev = 0; ev < existingVerticesInCut.Count; ev++) + { + if (Math.Approx3(verticesPositions[vertexIndex], existingVerticesInCut[ev])) + { + alreadyExists = true; + break; + } + } + if (alreadyExists) continue; } @@ -156,7 +165,16 @@ internal void UpdateMeshConnections() { if(existingVerticesInCut.Count > 0) { - if(existingVerticesInCut.Exists(vert => Math.Approx3(verticesPositions[vertexIndex], vert))) + bool alreadyExists = false; + for (int ev = 0; ev < existingVerticesInCut.Count; ev++) + { + if (Math.Approx3(verticesPositions[vertexIndex], existingVerticesInCut[ev])) + { + alreadyExists = true; + break; + } + } + if (alreadyExists) continue; } @@ -177,13 +195,22 @@ internal void UpdateMeshConnections() if(pathIndex >= 0) { - if(m_MeshConnections.Exists(tup => tup.item1 == pathIndex)) + int connIdx = -1; + for (int k = 0; k < m_MeshConnections.Count; k++) + { + if (m_MeshConnections[k].item1 == pathIndex) + { + connIdx = k; + break; + } + } + if(connIdx >= 0) { - var tuple = m_MeshConnections.Find(tup => tup.item1 == pathIndex); + var tuple = m_MeshConnections[connIdx]; if(Vector3.Distance(m_CutPath[tuple.item1].position, verticesPositions[tuple.item2]) > Vector3.Distance(m_CutPath[pathIndex].position, verticesPositions[vertexIndex])) { - m_MeshConnections.Remove(tuple); + m_MeshConnections.RemoveAt(connIdx); m_MeshConnections.Add(new SimpleTuple(pathIndex, vertexIndex)); } } @@ -255,9 +282,9 @@ void CheckPointInMesh() m_SnapedEdge = Edge.Empty; Vector3[] vertexPositions = m_Mesh.positionsInternal; - IList peripheralEdges = m_CurrentFace.edges; + List peripheralEdges = WingedEdge.SortEdgesByAdjacency(m_CurrentFace); if (m_TargetFace != null && m_CurrentFace != m_TargetFace) - peripheralEdges = m_TargetFace.edges; + peripheralEdges = WingedEdge.SortEdgesByAdjacency(m_TargetFace); for (int i = 0; i < peripheralEdges.Count; i++) { if ((m_TargetFace == null || m_TargetFace == m_CurrentFace) && m_SnappingPoint) diff --git a/Editor/EditorCore/CutTool.cs b/Editor/EditorCore/CutTool.cs index 884f43fdf..e5d0872cc 100644 --- a/Editor/EditorCore/CutTool.cs +++ b/Editor/EditorCore/CutTool.cs @@ -137,6 +137,8 @@ public override GUIContent toolbarIcon const string k_RectangleModePrefKey = "VertexInsertion.rectangleMode"; static readonly Color k_RectPreviewColor = new Color(1f, 1f, 0f, 0.4f); static readonly Color k_RectOutlineColor = new Color(1f, 1f, 0f, 1f); + readonly Vector3[] m_RectConvexPolygon = new Vector3[4]; + readonly Vector3[] m_RectPreviewPath = new Vector3[5]; public bool isALoop {