diff --git a/Editor/EditorCore/CutTool.Geometry.cs b/Editor/EditorCore/CutTool.Geometry.cs
new file mode 100644
index 000000000..75b0c5934
--- /dev/null
+++ b/Editor/EditorCore/CutTool.Geometry.cs
@@ -0,0 +1,473 @@
+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);
+
+ // 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++)
+ {
+ 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();
+
+ //Update mesh connections with new indexes
+ for(int i = 0; i connection = m_MeshConnections[i];
+ connection.item1 = vertexIndexMap[cutVertices[connection.item1]];
+ connection.item2 = vertexIndexMap[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);
+ 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
+ // For loop cuts, select only the central cutout so user can immediately manipulate it
+ MeshSelection.ClearElementSelection();
+ 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);
+
+ 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)
+ {
+ 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];
+
+ 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;
+
+ // 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
+ 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 ||
+ (hasConnectionSharedItem1 && sharedToUnique[vertexIndex] == connectionSharedItem1))
+ {
+ 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 ||
+ (hasConnectionSharedItem1 && sharedToUnique[vertexIndex] == connectionSharedItem1))
+ {
+ 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);
+ uniqueIdToVertexIndex = m_Mesh.sharedVertices;
+ sharedToUnique = m_Mesh.sharedVertexLookup;
+
+ float area;
+ if (!TryGetFaceArea(face, 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, IList uniqueIdToVertexIndex,
+ Dictionary sharedToUnique, out float area)
+ {
+ area = 0f;
+
+ if (face == null || face.indexesInternal == null)
+ return false;
+
+ Vector3[] vertices = m_Mesh.positionsInternal;
+ 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)
+ {
+ Vector3[] vertexPositions = m_Mesh.positionsInternal;
+ 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,
+ vertexPositions[peripheralEdges[i].a],
+ vertexPositions[peripheralEdges[i].b]);
+
+ 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..9a9bfccb2
--- /dev/null
+++ b/Editor/EditorCore/CutTool.Rectangle.cs
@@ -0,0 +1,263 @@
+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
+ {
+ ///
+ /// 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.
+ ///
+ 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);
+
+ Vector3 faceRight, faceUp;
+ 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 all 4 corners strictly on the face plane (coplanar)
+ Vector3 corner0 = start;
+ Vector3 corner1 = start + faceRight * rightDot;
+ Vector3 corner2 = start + faceRight * rightDot + faceUp * upDot;
+ Vector3 corner3 = start + faceUp * upDot;
+
+ // Snap only start point to grid, then project back onto face plane
+ if (m_SnapToGrid)
+ {
+ 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;
+ }
+
+ if (HasSignificantRectangle(corner0, corner2))
+ {
+ // Build cut path: 4 corners + close back to start to form a loop
+ UndoUtility.RecordObject(this, "Rectangle Cut");
+
+ m_CurrentPositionNormal = faceNormal;
+ 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_CurrentVertexTypes = VertexTypes.None;
+ if (m_SnapToGeometry)
+ CheckPointInMesh();
+ if (m_CurrentVertexTypes == VertexTypes.None)
+ m_CurrentVertexTypes = VertexTypes.NewVertex;
+ AddCurrentPositionToPath(false);
+
+ // Corner 2
+ m_CurrentPosition = corner2;
+ 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_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_CurrentVertexTypes = VertexTypes.VertexInShape;
+ AddCurrentPositionToPath(false);
+
+ // Don't auto-execute—let user click Complete button like point mode
+ RebuildCutShape(false);
+ }
+
+ 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;
+
+ // Compute the 4 corners in local space
+ Vector3 faceNormal = Math.Normal(m_Mesh, m_TargetFace);
+
+ Vector3 faceRight, faceUp;
+ GetFacePlaneAxes(faceNormal, out faceRight, out faceUp);
+
+ Vector3 diagonal = m_RectEndPoint - m_RectStartPoint;
+ float rightDot = Vector3.Dot(diagonal, faceRight);
+ float upDot = Vector3.Dot(diagonal, faceUp);
+
+ 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 (reuse cached arrays to avoid per-frame allocation)
+ Handles.color = k_RectPreviewColor;
+ 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;
+ 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)
+ {
+ 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..1b3067198
--- /dev/null
+++ b/Editor/EditorCore/CutTool.Snapping.cs
@@ -0,0 +1,406 @@
+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)
+ {
+ Vector3 snapped = ProBuilderSnapping.Snap(m_CurrentPosition, EditorSnapping.activeMoveSnapValue);
+ Plane facePlane = new Plane(m_CurrentPositionNormal, m_CurrentPosition);
+ m_CurrentPosition = facePlane.ClosestPointOnPlane(snapped);
+ }
+ }
+
+ ///
+ /// 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)
+ {
+ bool alreadyExists = false;
+ for (int ev = 0; ev < existingVerticesInCut.Count; ev++)
+ {
+ if (Math.Approx3(verticesPositions[vertexIndex], existingVerticesInCut[ev]))
+ {
+ alreadyExists = true;
+ break;
+ }
+ }
+ if (alreadyExists)
+ 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)
+ {
+ 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)
+ {
+ if(existingVerticesInCut.Count > 0)
+ {
+ bool alreadyExists = false;
+ for (int ev = 0; ev < existingVerticesInCut.Count; ev++)
+ {
+ if (Math.Approx3(verticesPositions[vertexIndex], existingVerticesInCut[ev]))
+ {
+ alreadyExists = true;
+ break;
+ }
+ }
+ if (alreadyExists)
+ 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)
+ {
+ 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[connIdx];
+ if(Vector3.Distance(m_CutPath[tuple.item1].position, verticesPositions[tuple.item2])
+ > Vector3.Distance(m_CutPath[pathIndex].position, verticesPositions[vertexIndex]))
+ {
+ m_MeshConnections.RemoveAt(connIdx);
+ 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 = 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;
+
+ Vector3[] vertexPositions = m_Mesh.positionsInternal;
+ 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(vertexPositions[peripheralEdges[i].a],
+ m_CurrentPosition,
+ snapDistance))
+ {
+ bestIndex = i;
+ snapedOnVertex = true;
+ break;
+ }
+ else
+ {
+ float dist = Math.DistancePointLineSegment(
+ m_CurrentPosition,
+ vertexPositions[peripheralEdges[i].a],
+ vertexPositions[peripheralEdges[i].b]);
+
+ 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(vertexPositions[peripheralEdges[i].a],
+ m_CurrentPosition,
+ 0.01f))
+ {
+ bestIndex = i;
+ snapedOnVertex = true;
+ break;
+ }
+ else
+ {
+ float dist = Math.DistancePointLineSegment(
+ m_CurrentPosition,
+ vertexPositions[peripheralEdges[i].a],
+ vertexPositions[peripheralEdges[i].b]);
+
+ 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,
+ vertexPositions[peripheralEdges[i].a],
+ vertexPositions[peripheralEdges[i].b]);
+
+ float vertexDist = Vector3.Distance(m_CurrentPosition,
+ vertexPositions[peripheralEdges[i].a]);
+
+ 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 = vertexPositions[peripheralEdges[bestIndex].a];
+ 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 = vertexPositions[peripheralEdges[bestIndex].a],
+ right = vertexPositions[peripheralEdges[bestIndex].b];
+
+ 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 -
+ 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 += vertexPositions[peripheralEdges[bestIndex].a];
+ }
+
+ 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..4a5de6744
--- /dev/null
+++ b/Editor/EditorCore/CutTool.UI.cs
@@ -0,0 +1,250 @@
+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;
+
+ EditorGUI.BeginChangeCheck();
+
+ m_RectangleMode = DoOverlayToggle(L10n.Tr("Rectangle Mode"), m_RectangleMode);
+ m_SnapToGrid = DoOverlayToggle(L10n.Tr("Snap to Grid"), m_SnapToGrid);
+ m_SnapToGeometry = DoOverlayToggle(L10n.Tr("Snap to existing edges and vertices"), 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);
+ }
+ 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)
+ 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_CutPath.Count > 1 && m_IsCutValid)
+ {
+ 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;
+ 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;
+ }
+
+ ///
+ /// 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)
+ {
+ 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 >= pos.Length)
+ {
+ m_MeshConnections.RemoveAt(i);
+ continue;
+ }
+ Handles.color = k_ConnectionsLineColor;
+ Handles.DrawDottedLine(trs.TransformPoint(m_CutPath[connection.item1].position),
+ trs.TransformPoint(pos[connection.item2]), 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..90584c395
--- /dev/null
+++ b/Editor/EditorCore/CutTool.Validation.cs
@@ -0,0 +1,143 @@
+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;
+
+ // 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;
+ GetFacePlaneAxes(faceNormal, out faceRight, out faceUp);
+
+ Vector2[] cutPath2D = new Vector2[m_CutPath.Count];
+ for (int i = 0; i < m_CutPath.Count; i++)
+ {
+ 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++)
+ {
+ 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++)
+ {
+ 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
+ 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 = cutPath2D[j];
+ Vector2 segment2End2D = cutPath2D[j + 1];
+
+ 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 = cutPath2D[connection.item1];
+ Vector2 segment2End2D = meshConnection2D[j];
+
+ 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 = 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 = cutPath2D[connection2.item1];
+ Vector2 segment2End2D = meshConnection2D[j];
+
+ 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 769f84dc1..27fd877dd 100644
--- a/Editor/EditorCore/CutTool.cs
+++ b/Editor/EditorCore/CutTool.cs
@@ -20,7 +20,7 @@ namespace UnityEditor.ProBuilder
{
[EditorTool("Cut Tool", typeof(ProBuilderMesh), typeof(PositionToolContext))]
[Icon("Packages/com.unity.probuilder/Editor Default Resources/Icons/Toolbar/CutTool.png")]
- class CutTool : EditorTool
+ partial class CutTool : EditorTool
{
ProBuilderMesh m_Mesh;
@@ -115,6 +115,7 @@ public CutVertexData(Vector3 position, Vector3 normal, VertexTypes types = Verte
Edge m_SnapedEdge = Edge.Empty;
bool m_SnapToGeometry;
+ bool m_SnapToGrid;
float m_SnappingDistance;
//Overlay fields
@@ -122,6 +123,18 @@ public CutVertexData(Vector3 position, Vector3 normal, VertexTypes types = Verte
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_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);
+ readonly Vector3[] m_RectConvexPolygon = new Vector3[4];
+ readonly Vector3[] m_RectPreviewPath = new Vector3[5];
+
public bool isALoop
{
get
@@ -157,6 +170,8 @@ Texture2D cursorTexture
}
}
+ public override bool gridSnapEnabled => true;
+
public override bool IsAvailable()
{
return MeshSelection.selectedObjectCount == 1;
@@ -169,7 +184,9 @@ 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 );
m_CutCursorTexture = IconUtility.GetIcon("Cursors/cutCursor");
m_CutAddCursorTexture = IconUtility.GetIcon("Cursors/cutCursor-add");
@@ -194,7 +211,8 @@ public override void OnActivated()
public override void OnWillBeDeactivated()
{
- ExecuteCut(false);
+ if(!m_RectangleMode && m_TargetFace != null && m_CutPath.Count > 1)
+ ExecuteCut(false);
Undo.undoRedoPerformed -= UndoRedoPerformed;
MeshSelection.objectSelectionChanged -= UpdateTarget;
@@ -207,16 +225,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_SelectedVertices = null;
- m_SelectedEdges = null;
+ m_RectDragging = false;
+ m_RectStartPoint = Vector3.positiveInfinity;
+ m_RectEndPoint = Vector3.positiveInfinity;
+
+ 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();
@@ -292,22 +338,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
///
@@ -338,7 +368,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 +379,11 @@ public override void OnToolGUI( EditorWindow window )
if(currentEvent.type == EventType.Repaint)
{
- DrawGuideLine();
+ if(m_RectangleMode)
+ DoRectanglePreview();
+ else
+ DrawGuideLine();
+
DoCurrentPointsGUI();
DoVisualCues();
@@ -378,64 +415,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_SnapToGeometry = DoOverlayToggle(L10n.Tr("Snap to existing edges and vertices"), m_SnapToGeometry);
- EditorPrefs.SetBool(k_SnapToGeometryPrefKey, m_SnapToGeometry);
-
- if(!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(GUILayout.Button(EditorGUIUtility.TrTextContent("Complete")))
- ExecuteCut();
-
- if(GUILayout.Button(EditorGUIUtility.TrTextContent("Cancel")))
- ExitTool();
- }
- }
-
- GUI.enabled = true;
- }
-
-
///
/// Handle key events
///
@@ -484,14 +463,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)
@@ -565,32 +544,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);
- }
- }
-
- 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));
+ if (TryPassThroughSelection(window, hasHitPosition))
+ return;
}
internal void AddCurrentPositionToPath(bool optimize = true)
@@ -612,962 +567,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();
- }
-
- ///
- /// 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);
- f.submeshIndex = m_TargetFace.submeshIndex;
-
- if(f == null)
- return new ActionResult(ActionResult.Status.Failure, L10n.Tr("Cut Shape is not valid"));
-
- 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();
-
- Clear();
-
- 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);
- newFace.submeshIndex = m_TargetFace.submeshIndex;
-
- 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;
-
- int polygonFirstVertex = polygonStart[0];
- int startIndex = cutIndexes.IndexOf(currentIndex);
-
- SimpleTuple connection = m_MeshConnections.Find(tup => sharedToUnique[tup.item2] == sharedToUnique[polygonFirstVertex]);
-
- 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] == sharedToUnique[polygonFirstVertex] ||
- 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] == sharedToUnique[polygonFirstVertex] ||
- 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
- facesToDelete = new List();
- Face bestFace = null;
- float bestArea = 0f;
- foreach(var closure in closureCandidates)
- {
- closure.AddRange(polygonStart);
-
- Face face = m_Mesh.CreatePolygon(closure, false);
- m_Mesh.GetVerticesInList(meshVertices);
- uniqueIdToVertexIndex = m_Mesh.sharedVertices;
- sharedToUnique = m_Mesh.sharedVertexLookup;
-
- 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);
- bestArea = area;
- bestFace = face;
- }
- else
- facesToDelete.Add(face);
- }
- 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);
- }
- }
- }
-
- return bestFace;
- }
-
- ///
- /// 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)
- {
- Vertex[] vertices = m_Mesh.GetVertices();
- foreach(var connection in m_MeshConnections)
- {
- 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;
- }
- }
- }
-
}
}