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; - } - } - } - } }