diff --git a/engine/Sandbox.Engine/Scene/Components/Mesh/PolygonMesh.cs b/engine/Sandbox.Engine/Scene/Components/Mesh/PolygonMesh.cs index c4de3efc..8dd31ec6 100644 --- a/engine/Sandbox.Engine/Scene/Components/Mesh/PolygonMesh.cs +++ b/engine/Sandbox.Engine/Scene/Components/Mesh/PolygonMesh.cs @@ -620,7 +620,7 @@ public sealed partial class PolygonMesh : IJsonConvert pOutVertexB = GetVertexPosition( hVertexB ); } - private bool AreEdgesCoLinear( HalfEdgeHandle hEdgeA, HalfEdgeHandle hEdgeB, float flAngleToleranceInDegrees ) + public bool AreEdgesCoLinear( HalfEdgeHandle hEdgeA, HalfEdgeHandle hEdgeB, float flAngleToleranceInDegrees ) { float flTolerance = MathF.Cos( MathF.Min( flAngleToleranceInDegrees, 180.0f ).DegreeToRadian() ); @@ -653,6 +653,13 @@ public sealed partial class PolygonMesh : IJsonConvert IsDirty = true; } + public void DissolveEdge( HalfEdgeHandle edge ) + { + Topology.DissolveEdge( edge, out _ ); + + IsDirty = true; + } + public void DissolveEdges( IReadOnlyList edges, bool bFaceMustBePlanar, DissolveRemoveVertexCondition removeCondition ) { const float flColinearTolerance = 5.0f; // Edges may be at an angle of up to this many degrees and still be considered co-linear @@ -727,6 +734,68 @@ public sealed partial class PolygonMesh : IJsonConvert IsDirty = true; } + public bool ComputeClosestPointOnEdge( VertexHandle hVertexA, VertexHandle hVertexB, Vector3 vTargetPoint, out float pOutBaseEdgeParam ) + { + pOutBaseEdgeParam = 0.0f; + var hEdge = FindEdgeConnectingVertices( hVertexA, hVertexB ); + if ( hEdge == HalfEdgeHandle.Invalid ) + return false; + + var vEdgePositions = new Vector3[2]; + vEdgePositions[0] = GetVertexPosition( hVertexA ); + vEdgePositions[1] = GetVertexPosition( hVertexB ); + int nNumPositions = vEdgePositions.Length; + + float flEdgeLength = 0.0f; + for ( int iPos = 1; iPos < nNumPositions; ++iPos ) + { + flEdgeLength += vEdgePositions[iPos - 1].Distance( vEdgePositions[iPos] ); + } + + Vector3 vClosestPointOnEdge = Vector3.Zero; + float flClosestPointParam = 0.0f; + float flMinDistanceSqr = float.MaxValue; + float flClosestSegmentParam = 0.0f; + float flBaseEdgeParam = 0.0f; + int nClosestSegment = -1; + + float flSegmentStart = 0.0f; + for ( int iPos = 1; iPos < nNumPositions; ++iPos ) + { + var vEdgePosA = vEdgePositions[iPos - 1]; + var vEdgePosB = vEdgePositions[iPos]; + + var vSegment = vEdgePosB - vEdgePosA; + float flSegmentLength = vSegment.Length; + float flSegmentEnd = flSegmentStart + flSegmentLength; + + CalcClosestPointOnLineSegment( vTargetPoint, vEdgePosA, vEdgePosB, out var vClosestPointOnSegment, out var flSegmentParam ); + + float flDistSqr = vClosestPointOnSegment.DistanceSquared( vTargetPoint ); + if ( flDistSqr < flMinDistanceSqr ) + { + flMinDistanceSqr = flDistSqr; + vClosestPointOnEdge = vClosestPointOnSegment; + flClosestSegmentParam = flSegmentParam; + nClosestSegment = iPos - 1; + float flPointDistance = flSegmentStart + flSegmentParam * flSegmentLength; + flClosestPointParam = flPointDistance / flEdgeLength; + + flBaseEdgeParam = MathX.Lerp( (iPos - 1) / (float)(nNumPositions - 1), iPos / (float)(nNumPositions - 1), flSegmentParam ); + } + + flSegmentStart = flSegmentEnd; + } + + Assert.True( nClosestSegment >= 0 ); + if ( nClosestSegment < 0 ) + return false; + + pOutBaseEdgeParam = flBaseEdgeParam; + + return true; + } + private void RemoveVerticesFromColinearEdgesInFace( FaceHandle hFace, float flColinearAngleTolerance ) { // Get all of the vertices in the face @@ -743,23 +812,192 @@ public sealed partial class PolygonMesh : IJsonConvert } } - private bool RemoveColinearVertex( VertexHandle hVertex, float flColinearAngleTolerance ) + bool RemoveColinearVertex( VertexHandle hVertex, float flColinearAngleTolerance ) + { + return RemoveColinearVertexAndUpdateTable( hVertex, null, flColinearAngleTolerance ); + } + + public bool RemoveColinearVertexAndUpdateTable( VertexHandle hVertex, SortedSet edgeTable, float flColinearAngleTolerance = 5.0f ) { Topology.GetFullEdgesConnectedToVertex( hVertex, out var edgesConnectedToVertex ); - if ( edgesConnectedToVertex.Count == 2 ) + if ( edgesConnectedToVertex is not null && edgesConnectedToVertex.Count == 2 ) { if ( AreEdgesCoLinear( edgesConnectedToVertex[0], edgesConnectedToVertex[1], flColinearAngleTolerance ) ) { + // Were either of the edges in the table + bool bEdgeInTable = false; + if ( edgeTable is not null ) + { + if ( edgeTable.Contains( edgesConnectedToVertex[0] ) || + edgeTable.Contains( edgesConnectedToVertex[1] ) ) + { + bEdgeInTable = true; + } + } + + // Get the vertices at the ends of each edge opposite the vertex about to be removed. + GetVerticesConnectedToEdge( edgesConnectedToVertex[0], out var hVertexA, out var hVertexB ); + var hVertex0 = (hVertexA == hVertex) ? hVertexB : hVertexA; + + GetVerticesConnectedToEdge( edgesConnectedToVertex[1], out hVertexA, out hVertexB ); + var hVertex1 = (hVertexA == hVertex) ? hVertexB : hVertexA; + // Remove the vertex, combining the two edges into a single edge if ( Topology.RemoveVertex( hVertex, true ) ) + { + // Remove the two old edges from the table and add the new edge + if ( bEdgeInTable ) + { + edgeTable.Remove( edgesConnectedToVertex[0] ); + edgeTable.Remove( edgesConnectedToVertex[1] ); + var hCombinedEdge = FindEdgeConnectingVertices( hVertex0, hVertex1 ); + if ( hCombinedEdge != HalfEdgeHandle.Invalid ) + { + edgeTable.Add( hCombinedEdge ); + } + } + return true; + } } } return false; } + public bool GetEdgesConnectedToVertex( VertexHandle hVertex, out List edges ) + { + return Topology.GetFullEdgesConnectedToVertex( hVertex, out edges ); + } + + static float CalcClosestPointToLineT( Vector3 P, Vector3 vLineA, Vector3 vLineB, out Vector3 vDir ) + { + vDir = vLineB - vLineA; + var div = vDir.Dot( vDir ); + return div < 0.00001f ? 0.0f : (vDir.Dot( P ) - vDir.Dot( vLineA )) / div; + } + + static void CalcClosestPointOnLine( Vector3 P, Vector3 vLineA, Vector3 vLineB, out Vector3 vClosest, out float outT ) + { + outT = CalcClosestPointToLineT( P, vLineA, vLineB, out var vDir ); + vClosest = vLineA + vDir * outT; + } + + public VertexHandle CreateEdgesConnectingVertexToPoint( VertexHandle hStartVertex, Vector3 vTargetPosition, out List pOutEdgeList, out bool pOutIsLastEdgeConnector, SortedSet pEdgeTable ) + { + const float flTolerance = 0.001f; + + pOutEdgeList = []; + pOutIsLastEdgeConnector = false; + + var hCurrentVertex = hStartVertex; + var hTargetVertex = VertexHandle.Invalid; + + while ( hTargetVertex == VertexHandle.Invalid ) + { + var hNextVertex = VertexHandle.Invalid; + + if ( FindCutEdgeIntersection( hCurrentVertex, vTargetPosition, out var hIntersectionEdge, out var hIntersectionFace, out var vIntersectionPoint ) ) + { + GetVerticesConnectedToEdge( hIntersectionEdge, out var hVertexA, out var hVertexB ); + + var vPositionA = GetVertexPosition( hVertexA ); + var vPositionB = GetVertexPosition( hVertexB ); + + CalcClosestPointOnLineSegment( vIntersectionPoint, vPositionA, vPositionB, out _, out var flParam ); + + if ( flParam < flTolerance ) + { + hNextVertex = hVertexA; + } + else if ( flParam > (1.0f - flTolerance) ) + { + hNextVertex = hVertexB; + } + else + { + AddVertexToEdgeAndUpdateTable( hVertexA, hVertexB, flParam, out hNextVertex, pEdgeTable ); + } + } + + if ( hNextVertex == VertexHandle.Invalid ) + break; + + var hTargetEdges = new HalfEdgeHandle[2]; + hTargetEdges[0] = FindEdgeConnectingVertices( hCurrentVertex, hNextVertex ); + hTargetEdges[1] = HalfEdgeHandle.Invalid; + + if ( hTargetEdges[0] == HalfEdgeHandle.Invalid ) + { + if ( IsLineBetweenVerticesInsideFace( hIntersectionFace, hCurrentVertex, hNextVertex ) ) + { + AddEdgeToFace( hIntersectionFace, hCurrentVertex, hNextVertex, out hTargetEdges[0] ); + } + + if ( pEdgeTable is not null && (hTargetEdges[0] != HalfEdgeHandle.Invalid) ) + { + pEdgeTable.Add( hTargetEdges[0] ); + } + } + + if ( hTargetEdges[0] != HalfEdgeHandle.Invalid ) + { + var vPositionA = GetVertexPosition( hCurrentVertex ); + var vPositionB = GetVertexPosition( hNextVertex ); + + CalcClosestPointOnLine( vTargetPosition, vPositionA, vPositionB, out _, out var flParam ); + if ( (flParam > -flTolerance) && (flParam < (1.0f + flTolerance)) ) + { + if ( flParam < flTolerance ) + { + hTargetVertex = hCurrentVertex; + } + else if ( flParam > (1.0f - flTolerance) ) + { + hTargetVertex = hNextVertex; + } + else + { + if ( AddVertexToEdgeAndUpdateTable( hCurrentVertex, hNextVertex, flParam, out hTargetVertex, pEdgeTable ) ) + { + hTargetEdges[0] = FindEdgeConnectingVertices( hCurrentVertex, hTargetVertex ); + hTargetEdges[1] = FindEdgeConnectingVertices( hTargetVertex, hNextVertex ); + + if ( pEdgeTable is not null && pEdgeTable.Contains( hTargetEdges[1] ) ) + { + pOutIsLastEdgeConnector = true; + } + } + else + { + Assert.True( hTargetVertex != VertexHandle.Invalid ); + break; + } + } + } + } + + if ( hTargetEdges[0] != HalfEdgeHandle.Invalid ) + { + pOutEdgeList.Add( hTargetEdges[0] ); + } + if ( hTargetEdges[1] != HalfEdgeHandle.Invalid ) + { + pOutEdgeList.Add( hTargetEdges[1] ); + } + + Assert.True( hNextVertex != hStartVertex ); + Assert.True( hNextVertex != hCurrentVertex ); + if ( (hNextVertex == hStartVertex) || (hNextVertex == hCurrentVertex) ) + break; + + hCurrentVertex = hNextVertex; + } + + return hTargetVertex; + } + public enum DissolveRemoveVertexCondition { None, // Never remove vertices @@ -1396,6 +1634,43 @@ public sealed partial class PolygonMesh : IJsonConvert return true; } + public bool AddVertexToEdgeAndUpdateTable( VertexHandle hVertexA, VertexHandle hVertexB, float flParam, out VertexHandle pNewVertex, SortedSet pEdgeTable ) + { + pNewVertex = VertexHandle.Invalid; + + bool bOriginalEdgeInTable = false; + var hOriginalEdge = HalfEdgeHandle.Invalid; + + if ( pEdgeTable is not null ) + { + var hEdge = FindEdgeConnectingVertices( hVertexA, hVertexB ); + if ( pEdgeTable.Contains( hEdge ) ) + { + bOriginalEdgeInTable = true; + } + } + + if ( AddVertexToEdge( hVertexA, hVertexB, flParam, out var hNewVertex ) ) + { + if ( bOriginalEdgeInTable ) + { + Topology.GetFullEdgesConnectedToVertex( hNewVertex, out var connectedEdges ); + if ( connectedEdges.Count == 2 ) + { + pEdgeTable.Remove( hOriginalEdge ); + pEdgeTable.Add( connectedEdges[0] ); + pEdgeTable.Add( connectedEdges[1] ); + } + } + + pNewVertex = hNewVertex; + + return true; + } + + return false; + } + public bool RemoveVertex( VertexHandle hVertex, bool removeFreeVerts ) { return Topology.RemoveVertex( hVertex, removeFreeVerts ); @@ -2386,6 +2661,8 @@ public sealed partial class PolygonMesh : IJsonConvert TextureCoord[hFaceVertex] = texCoord; } } + + IsDirty = true; } private static bool CalcTextureBasisFromUVs( Vector3[] vVertPos, Vector2[] vTexCoord, out Vector3 vOutU, out Vector3 vOutV ) @@ -3039,7 +3316,7 @@ public sealed partial class PolygonMesh : IJsonConvert return Topology.GetFullEdgesConnectedToFace( hFace, out edges ); } - private bool GetVerticesConnectedToEdge( HalfEdgeHandle hEdge, FaceHandle hFace, out VertexHandle hOutVertexA, out VertexHandle hOutVertexB ) + public bool GetVerticesConnectedToEdge( HalfEdgeHandle hEdge, FaceHandle hFace, out VertexHandle hOutVertexA, out VertexHandle hOutVertexB ) { hOutVertexA = VertexHandle.Invalid; hOutVertexB = VertexHandle.Invalid; @@ -3114,7 +3391,7 @@ public sealed partial class PolygonMesh : IJsonConvert return HalfEdgeHandle.Invalid; } - private bool GetVerticesConnectedToFace( FaceHandle hFace, out VertexHandle[] vertices ) + public bool GetVerticesConnectedToFace( FaceHandle hFace, out VertexHandle[] vertices ) { return Topology.GetVerticesConnectedToFace( hFace, out vertices ); } @@ -4221,6 +4498,115 @@ public sealed partial class PolygonMesh : IJsonConvert .ToArray(); } + bool FindCutEdgeIntersection( VertexHandle hVertex, Vector3 targetPosition, out HalfEdgeHandle outEdge, out FaceHandle outFace, out Vector3 outPosition ) + { + outEdge = HalfEdgeHandle.Invalid; + outFace = FaceHandle.Invalid; + outPosition = default; + + GetVertexPosition( hVertex, Transform.Zero, out var vCurrentPosition ); + var vDir = (targetPosition - vCurrentPosition).Normal; + + GetFacesConnectedToVertex( hVertex, out var connectedFaces ); + + var hBestFace = FaceHandle.Invalid; + var hBestVertexA = VertexHandle.Invalid; + var hBestVertexB = VertexHandle.Invalid; + var vBestPoint = Vector3.Zero; + var flMinDistance = float.MaxValue; + + int nNumFaces = connectedFaces.Count; + for ( int iFace = 0; iFace < nNumFaces; ++iFace ) + { + var hFace = connectedFaces[iFace]; + + ComputeFaceNormal( hFace, out var vFaceNormal ); + + if ( MathF.Abs( vFaceNormal.Dot( vDir ) ) > 0.5f ) + continue; + + var vCutPlaneNormal = vFaceNormal.Cross( vDir ).Normal; + var cutPlane = new Plane( vCurrentPosition, vCutPlaneNormal ); + var basePlane = new Plane( vCurrentPosition, vDir ); + + var hStartFaceVertex = FindFaceVertexConnectedToVertex( hVertex, hFace ); + var hFaceVertexA = GetNextVertexInFace( hStartFaceVertex ); + var hFaceVertexB = GetNextVertexInFace( hFaceVertexA ); + + var hBestVertexForFaceA = VertexHandle.Invalid; + var hBestVertexForFaceB = VertexHandle.Invalid; + var vBestPointForFace = Vector3.Zero; + var flMinBasePlaneDistance = float.MaxValue; + + while ( hFaceVertexB != hStartFaceVertex ) + { + var hVertexA = GetVertexConnectedToFaceVertex( hFaceVertexA ); + var hVertexB = GetVertexConnectedToFaceVertex( hFaceVertexB ); + + if ( (hVertexA != hVertex) && (hVertexB != hVertex) ) + { + var vPositionA = GetVertexPosition( hVertexA ); + var vPositionB = GetVertexPosition( hVertexB ); + var vIntersection = cutPlane.IntersectLine( vPositionA, vPositionB ); + if ( vIntersection.HasValue ) + { + float flBasePlaneDistance = basePlane.GetDistance( vIntersection.Value ); + + if ( (flBasePlaneDistance >= 0) && (flBasePlaneDistance <= flMinBasePlaneDistance) ) + { + var vAB = vPositionB - vPositionA; + var vCross = vDir.Cross( vAB ); + + if ( vCross.Dot( vFaceNormal ) > 0.0f ) + { + hBestVertexForFaceA = hVertexA; + hBestVertexForFaceB = hVertexB; + vBestPointForFace = vIntersection.Value; + } + else if ( flBasePlaneDistance < flMinBasePlaneDistance ) + { + hBestVertexForFaceA = VertexHandle.Invalid; + hBestVertexForFaceB = VertexHandle.Invalid; + vBestPointForFace = Vector3.Zero; + } + + flMinBasePlaneDistance = flBasePlaneDistance; + } + } + } + + hFaceVertexA = hFaceVertexB; + hFaceVertexB = GetNextVertexInFace( hFaceVertexB ); + } + + var flFaceTargetDistance = vBestPointForFace.Distance( targetPosition ); + if ( (hBestVertexForFaceA != VertexHandle.Invalid) && + (hBestVertexForFaceB != VertexHandle.Invalid) && + (flFaceTargetDistance < flMinDistance) ) + { + hBestFace = hFace; + hBestVertexA = hBestVertexForFaceA; + hBestVertexB = hBestVertexForFaceB; + vBestPoint = vBestPointForFace; + flMinDistance = flFaceTargetDistance; + } + } + + if ( hBestFace == FaceHandle.Invalid ) + return false; + + outEdge = FindEdgeConnectingVertices( hBestVertexA, hBestVertexB ); + outFace = hBestFace; + outPosition = vBestPoint; + + return true; + } + + public void GetFacesConnectedToEdge( HalfEdgeHandle hEdge, out FaceHandle hOutFaceA, out FaceHandle hOutFaceB ) + { + Topology.GetFacesConnectedToFullEdge( hEdge, out hOutFaceA, out hOutFaceB ); + } + private static readonly Vector3[] FaceNormals = { new( 0, 0, 1 ), diff --git a/engine/Sandbox.System/Math/Plane.cs b/engine/Sandbox.System/Math/Plane.cs index 5521ec65..31a50ea5 100644 --- a/engine/Sandbox.System/Math/Plane.cs +++ b/engine/Sandbox.System/Math/Plane.cs @@ -179,11 +179,21 @@ public struct Plane : System.IEquatable float d1 = GetDistance( start ); float d2 = GetDistance( end ); - if ( MathF.Abs( d1 ) < 0.001f ) return start; - if ( MathF.Abs( d1 - d2 ) < 0.001f ) return default; + const float eps = 0.001f; + + if ( MathF.Abs( d1 - d2 ) < eps ) + { + if ( MathF.Abs( d1 ) < eps ) + return start; + + return default; + } float t = -d1 / (d2 - d1); - return (t is >= 0.0f and <= 1.0f) ? start + (end - start) * t : default; + if ( t >= 0.0f && t <= 1.0f ) + return start + (end - start) * t; + + return default; } /// diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.Apply.cs b/game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.Apply.cs new file mode 100644 index 00000000..91ab0ed8 --- /dev/null +++ b/game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.Apply.cs @@ -0,0 +1,293 @@ +using HalfEdgeMesh; +using Sandbox.Diagnostics; + +namespace Editor.MeshEditor; + +partial class EdgeCutTool +{ + sealed class HalfEdgeHandleComparer : IComparer + { + public static readonly HalfEdgeHandleComparer Instance = new(); + + public int Compare( HalfEdgeHandle a, HalfEdgeHandle b ) + { + return a.Index.CompareTo( b.Index ); + } + } + + void Cancel() + { + if ( _cutPoints.Count == 0 ) + { + EditorToolManager.SetSubTool( _tool ); + } + + _cutPoints.Clear(); + } + + void Apply() + { + if ( _cutPoints.Count <= 1 ) return; + + var components = new HashSet( _cutPoints.Count ); + foreach ( var cutPoint in _cutPoints ) + { + var component = cutPoint.Face.Component; + if ( component.IsValid() ) components.Add( component ); + } + + using var undoScope = SceneEditorSession.Active.UndoScope( "Apply Edge Cut" ) + .WithComponentChanges( components ) + .Push(); + + var vertices = new List(); + var edges = new List(); + if ( ApplyCut( vertices, edges ) == false ) return; + + var selection = SceneEditorSession.Active.Selection; + selection.Clear(); + + foreach ( var vertex in vertices ) selection.Add( vertex ); + foreach ( var edge in edges ) selection.Add( edge ); + + foreach ( var component in components ) + { + var mesh = component.Mesh; + foreach ( var edge in edges ) + { + mesh.GetFacesConnectedToEdge( edge.Handle, out var faceA, out var faceB ); + selection.Add( new MeshFace( component, faceA ) ); + selection.Add( new MeshFace( component, faceB ) ); + } + } + + EditorToolManager.SetSubTool( _tool ); + } + + bool ApplyCut( List outCutPathVertices, List outCutPathEdges ) + { + var cutPoints = _cutPoints; + if ( cutPoints.Count == 0 ) return false; + + var components = new List( cutPoints.Count ); + var meshes = new List( cutPoints.Count ); + + foreach ( var cp in cutPoints ) + { + var component = cp.Face.Component; + if ( !component.IsValid() ) continue; + + var mesh = component.Mesh; + if ( meshes.Contains( mesh ) ) continue; + + meshes.Add( mesh ); + components.Add( component ); + } + + var edgeTables = new List>( meshes.Count ); + for ( int i = 0; i < meshes.Count; i++ ) edgeTables.Add( new SortedSet( HalfEdgeHandleComparer.Instance ) ); + + int startIndex = 0; + MeshVertex startVertex = default; + MeshEdge edgeToRemove = default; + + while ( startIndex < cutPoints.Count ) + { + while ( !startVertex.IsValid() && startIndex < cutPoints.Count ) + { + var startPoint = cutPoints[startIndex]; + + if ( startPoint.IsValid() ) + { + if ( startPoint.Edge.IsValid() ) + { + var component = startPoint.Edge.Component; + int meshIndex = components.IndexOf( component ); + Assert.True( meshIndex != -1 ); + + var hNewVertex = AddCutToEdge( startPoint.Edge, startPoint.BasePosition, edgeTables[meshIndex] ); + startVertex = new MeshVertex( component, hNewVertex ); + break; + } + + if ( startPoint.Vertex.IsValid() ) + { + startVertex = startPoint.Vertex; + break; + } + } + + startIndex++; + } + + int endIndex = startIndex + 1; + MeshVertex endVertex = default; + + if ( endIndex < cutPoints.Count ) + { + var endPoint = cutPoints[endIndex]; + if ( endPoint.Face.IsValid() && endPoint.Face.Component == startVertex.Component ) + { + var component = startVertex.Component; + var mesh = component.Mesh; + int meshIndex = components.IndexOf( component ); + Assert.True( meshIndex != -1 ); + + var edgeTable = edgeTables[meshIndex]; + var targetVertex = mesh.CreateEdgesConnectingVertexToPoint( startVertex.Handle, endPoint.BasePosition, + out var segmentEdges, out var isLastConnector, edgeTable ); + + if ( edgeToRemove.IsValid() && !segmentEdges.Contains( edgeToRemove.Handle ) ) + { + mesh.GetVerticesConnectedToEdge( edgeToRemove.Handle, out var a, out var b ); + mesh.DissolveEdge( edgeToRemove.Handle ); + edgeTable.Remove( edgeToRemove.Handle ); + mesh.RemoveColinearVertexAndUpdateTable( a, edgeTable ); + mesh.RemoveColinearVertexAndUpdateTable( b, edgeTable ); + } + + if ( endPoint.Face.IsValid() && !endPoint.Vertex.IsValid() && !endPoint.Edge.IsValid() && segmentEdges.Count > 1 && isLastConnector ) + { + edgeToRemove = new MeshEdge( component, segmentEdges[^1] ); + } + + endVertex = new MeshVertex( component, targetVertex ); + } + } + + startIndex = endIndex; + startVertex = endVertex; + } + + if ( outCutPathEdges is not null || outCutPathVertices is not null ) + { + var numMeshes = meshes.Count; + var totalEdgeCount = 0; + + foreach ( var edgeTable in edgeTables ) + { + totalEdgeCount += edgeTable.Count; + } + + if ( outCutPathEdges is not null ) + { + outCutPathEdges.Clear(); + outCutPathEdges.EnsureCapacity( totalEdgeCount ); + } + + var vertexSet = new HashSet( totalEdgeCount * 2 ); + + for ( int i = 0; i < numMeshes; ++i ) + { + var component = components[i]; + var mesh = component.Mesh; + var edgeTable = edgeTables[i]; + + foreach ( var hEdge in edgeTable ) + { + if ( hEdge.IsValid == false ) continue; + + outCutPathEdges?.Add( new MeshEdge( component, hEdge ) ); + + if ( outCutPathVertices is not null ) + { + mesh.GetVerticesConnectedToEdge( hEdge, out var hVertexA, out var hVertexB ); + vertexSet.Add( new MeshVertex( component, hVertexA ) ); + vertexSet.Add( new MeshVertex( component, hVertexB ) ); + } + } + } + + if ( outCutPathVertices is not null ) + { + outCutPathVertices.Clear(); + outCutPathVertices.EnsureCapacity( vertexSet.Count ); + + foreach ( var hVertex in vertexSet ) + { + outCutPathVertices.Add( hVertex ); + } + } + } + + foreach ( var mesh in meshes ) + { + mesh.ComputeFaceTextureCoordinatesFromParameters(); + } + + return true; + } + + static MeshFace FindSharedFace( MeshCutPoint cutPointA, MeshCutPoint cutPointB ) + { + if ( cutPointA.IsValid() == false ) return default; + if ( cutPointB.IsValid() == false ) return default; + if ( cutPointA.Component != cutPointB.Component ) return default; + + cutPointA.GetConnectedFaces( out var connectedFacesA ); + cutPointB.GetConnectedFaces( out var connectedFacesB ); + + foreach ( var face in connectedFacesA ) + { + if ( face.IsValid && connectedFacesB.Contains( face ) ) + { + return new MeshFace( cutPointA.Component, face ); + } + } + + return default; + } + + static VertexHandle AddCutToEdge( MeshEdge edge, Vector3 targetPosition, SortedSet edgeTable ) + { + if ( !edge.Component.IsValid() ) return VertexHandle.Invalid; + + var mesh = edge.Component.Mesh; + var visited = new List( 32 ); + var current = edge.Handle; + const float eps = 0.001f; + + while ( current != HalfEdgeHandle.Invalid ) + { + mesh.GetVerticesConnectedToEdge( current, out var a, out var b ); + mesh.GetVertexPosition( a, Transform.Zero, out var pa ); + mesh.GetVertexPosition( b, Transform.Zero, out var pb ); + ClosestPointOnLine( targetPosition, pa, pb, out _, out var t ); + + VertexHandle next; + if ( t > 1f + eps ) next = b; + else if ( t < -eps ) next = a; + else if ( t <= eps ) return a; + else if ( t >= 1f - eps ) return b; + else + { + mesh.AddVertexToEdgeAndUpdateTable( a, b, t, out var v, edgeTable ); + return v; + } + + visited.Add( current ); + var prev = current; + current = HalfEdgeHandle.Invalid; + + mesh.GetEdgesConnectedToVertex( next, out var edges ); + foreach ( var e in edges ) + { + if ( !visited.Contains( e ) && mesh.AreEdgesCoLinear( e, prev, 1.0f ) ) + { + current = e; + break; + } + } + } + + return VertexHandle.Invalid; + } + + static void ClosestPointOnLine( Vector3 p, Vector3 a, Vector3 b, out Vector3 closest, out float t ) + { + var d = b - a; + var div = d.Dot( d ); + t = div < 1e-5f ? 0f : (d.Dot( p ) - d.Dot( a )) / div; + closest = a + d * t; + } +} diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.Gizmo.cs b/game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.Gizmo.cs new file mode 100644 index 00000000..8cc88bea --- /dev/null +++ b/game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.Gizmo.cs @@ -0,0 +1,118 @@ + +namespace Editor.MeshEditor; + +partial class EdgeCutTool +{ + void DrawCutPoints() + { + using ( Gizmo.Scope( "Points" ) ) + { + Gizmo.Draw.IgnoreDepth = true; + + if ( _cutPoints.Count > 0 ) + { + Gizmo.Draw.LineThickness = 2; + Gizmo.Draw.Color = new Color( 0.3137f, 0.7843f, 1.0f, 1f ); + for ( int i = 1; i < _cutPoints.Count; i++ ) + { + Gizmo.Draw.Line( _cutPoints[i - 1].WorldPosition, _cutPoints[i].WorldPosition ); + } + } + + Gizmo.Draw.Color = Color.White; + + foreach ( var cutPoint in _cutPoints ) + { + Gizmo.Draw.Sprite( cutPoint.WorldPosition, 10, null, false ); + } + } + } + + void DrawPreview() + { + if ( _previewCutPoint.IsValid() == false ) return; + + var mesh = _previewCutPoint.Face.Component; + if ( _hoveredMesh != mesh ) _hoveredMesh = mesh; + + var edge = _previewCutPoint.Edge; + if ( edge.IsValid() ) + { + using ( Gizmo.Scope( "Edge Hover", _previewCutPoint.Edge.Transform ) ) + { + Gizmo.Draw.IgnoreDepth = true; + Gizmo.Draw.Color = Color.Green; + Gizmo.Draw.LineThickness = 4; + Gizmo.Draw.Line( edge.Line ); + } + } + + using ( Gizmo.Scope( "Point" ) ) + { + Gizmo.Draw.IgnoreDepth = true; + + if ( _cutPoints.Count > 0 ) + { + var lastCutPoint = _cutPoints.Last(); + Gizmo.Draw.LineThickness = 4; + Gizmo.Draw.Color = Color.White; + Gizmo.Draw.Line( _previewCutPoint.WorldPosition, lastCutPoint.WorldPosition ); + } + + Gizmo.Draw.Color = Color.White; + Gizmo.Draw.Sprite( _previewCutPoint.WorldPosition, 10, null, false ); + } + } + + static void DrawMesh( MeshComponent mesh ) + { + if ( mesh.IsValid() == false ) return; + + using ( Gizmo.ObjectScope( mesh.GameObject, mesh.WorldTransform ) ) + { + using ( Gizmo.Scope( "Edges" ) ) + { + var edgeColor = new Color( 0.3137f, 0.7843f, 1.0f, 1f ); + + Gizmo.Draw.LineThickness = 1; + Gizmo.Draw.IgnoreDepth = true; + Gizmo.Draw.Color = edgeColor.Darken( 0.3f ).WithAlpha( 0.2f ); + + foreach ( var v in mesh.Mesh.GetEdges() ) + { + Gizmo.Draw.Line( v ); + } + + Gizmo.Draw.Color = edgeColor; + Gizmo.Draw.IgnoreDepth = false; + Gizmo.Draw.LineThickness = 2; + + foreach ( var v in mesh.Mesh.GetEdges() ) + { + Gizmo.Draw.Line( v ); + } + } + + using ( Gizmo.Scope( "Vertices" ) ) + { + var vertexColor = new Color( 1.0f, 1.0f, 0.3f, 1f ); + + Gizmo.Draw.IgnoreDepth = true; + Gizmo.Draw.Color = vertexColor.Darken( 0.3f ).WithAlpha( 0.2f ); + + foreach ( var v in mesh.Mesh.GetVertexPositions() ) + { + Gizmo.Draw.Sprite( v, 8, null, false ); + } + + Gizmo.Draw.Color = vertexColor; + Gizmo.Draw.IgnoreDepth = false; + + foreach ( var v in mesh.Mesh.GetVertexPositions() ) + { + Gizmo.Draw.Sprite( v, 8, null, false ); + } + } + } + } +} diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.Snapping.cs b/game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.Snapping.cs new file mode 100644 index 00000000..29e7598a --- /dev/null +++ b/game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.Snapping.cs @@ -0,0 +1,397 @@ +using HalfEdgeMesh; +using System.Runtime.InteropServices; + +namespace Editor.MeshEditor; + +partial class EdgeCutTool +{ + enum EdgeCutAlignment { None, Grid, Perpendicular } + enum PolygonComponentType { Invalid = -1, Vertex, Edge, Face } + + struct SnapPoint + { + public Vector3 Position; + public EdgeCutAlignment Alignment; + public PolygonComponentType Type; + public VertexHandle Vertex; + public HalfEdgeHandle Edge; + + public SnapPoint( Vector3 pos, EdgeCutAlignment align ) + { + Position = pos; + Alignment = align; + Type = PolygonComponentType.Face; + Vertex = default; + Edge = default; + } + + public SnapPoint( VertexHandle v, Vector3 pos ) + { + Position = pos; + Alignment = EdgeCutAlignment.None; + Type = PolygonComponentType.Vertex; + Vertex = v; + Edge = default; + } + + public SnapPoint( HalfEdgeHandle e, Vector3 pos, EdgeCutAlignment align ) + { + Position = pos; + Alignment = align; + Type = PolygonComponentType.Edge; + Vertex = default; + Edge = e; + } + } + + static bool ShouldSnap() => Gizmo.Settings.SnapToGrid != Gizmo.IsCtrlPressed; + + MeshCutPoint FindSnappedCutPoint() + { + var face = MeshTrace.TraceFace( SelectionSampleRadius, out var point ); + if ( !face.IsValid() ) return default; + + GenerateSnapPoints( face, point, out var snapPoints ); + return SnapCutPoint( face, point, snapPoints ); + } + + static MeshCutPoint SnapCutPoint( MeshFace face, Vector3 target, List snaps ) + { + if ( snaps.Count == 0 ) return default; + + var best = snaps.OrderBy( s => target.DistanceSquared( s.Position ) ).First(); + return best.Type switch + { + PolygonComponentType.Vertex => new( face, new MeshVertex( face.Component, best.Vertex ) ), + PolygonComponentType.Edge => new( face, new MeshEdge( face.Component, best.Edge ), best.Position ), + PolygonComponentType.Face => new( face, best.Position ), + _ => default + }; + } + + static void GenerateVertexSnapPoints( MeshFace face, List snaps ) + { + if ( !face.IsValid() ) return; + + var mesh = face.Component.Mesh; + var transform = face.Component.WorldTransform; + + mesh.GetVerticesConnectedToFace( face.Handle, out var vertices ); + + foreach ( var v in vertices ) + { + mesh.GetVertexPosition( v, transform, out var pos ); + snaps.Add( new SnapPoint( v, pos ) ); + } + } + + void GenerateEdgeSnapPoints( MeshFace face, Vector3 target, List snaps ) + { + if ( !face.IsValid() ) return; + + var component = face.Component; + if ( !component.IsValid() ) return; + + var mesh = component.Mesh; + var faceHandle = face.Handle; + + var hasPrevious = _cutPoints.Count > 0; + var previous = hasPrevious ? _cutPoints.Last() : default; + var perpendicularNormal = Vector3.Zero; + var hasPerpendicular = hasPrevious && ComputePerpendicularPlaneNormalForCutPoint( previous, out perpendicularNormal ); + + mesh.GetEdgesConnectedToFace( faceHandle, out var edges ); + + for ( var i = 0; i < edges.Count; i++ ) + { + var edge = edges[i]; + + mesh.GetVerticesConnectedToEdge( edge, out var vA, out var vB ); + mesh.GetVertexPosition( vA, component.WorldTransform, out var p0 ); + mesh.GetVertexPosition( vB, component.WorldTransform, out var p1 ); + + snaps.Add( new SnapPoint( edge, p0.LerpTo( p1, 0.5f ), EdgeCutAlignment.None ) ); + + if ( SnapToEdge( target, p0, p1, float.MaxValue, true, out var edgeSnap ) ) + snaps.Add( new SnapPoint( edge, edgeSnap, EdgeCutAlignment.None ) ); + + if ( !hasPrevious || previous.Component != component ) + continue; + + var prevPos = previous.WorldPosition; + + if ( ComputePlaneIntersectionSnapPoint( Vector3.Right, prevPos, target, p0, p1, out var hit ) ) + snaps.Add( new SnapPoint( edge, hit, EdgeCutAlignment.Grid ) ); + + if ( ComputePlaneIntersectionSnapPoint( Vector3.Up, prevPos, target, p0, p1, out hit ) ) + snaps.Add( new SnapPoint( edge, hit, EdgeCutAlignment.Grid ) ); + + if ( ComputePlaneIntersectionSnapPoint( Vector3.Forward, prevPos, target, p0, p1, out hit ) ) + snaps.Add( new SnapPoint( edge, hit, EdgeCutAlignment.Grid ) ); + + if ( hasPerpendicular && ComputePlaneIntersectionSnapPoint( perpendicularNormal, prevPos, target, p0, p1, out hit ) ) + { + snaps.Add( new SnapPoint( edge, hit, EdgeCutAlignment.Perpendicular ) ); + } + } + } + + static void GenerateFaceSnapPoints( MeshFace face, Vector3 target, List snaps ) + { + if ( !face.IsValid() ) return; + + var component = face.Component; + if ( !component.IsValid() ) return; + + var mesh = component.Mesh; + var faceHandle = face.Handle; + + var vertices = mesh.GetFaceVertexPositions( faceHandle, component.WorldTransform ).ToList(); + var spacing = Gizmo.Settings.GridSpacing; + + var min = new Vector3( + MathF.Floor( target.x / spacing ) * spacing, + MathF.Floor( target.y / spacing ) * spacing, + MathF.Floor( target.z / spacing ) * spacing ); + + var max = new Vector3( + MathF.Ceiling( target.x / spacing ) * spacing, + MathF.Ceiling( target.y / spacing ) * spacing, + MathF.Ceiling( target.z / spacing ) * spacing ); + + var gridEdges = new Line[] + { + new( new( min.x - 1, min.y, min.z ), new( max.x + 1, min.y, min.z ) ), + new( new( min.x - 1, min.y, max.z ), new( max.x + 1, min.y, max.z ) ), + new( new( min.x - 1, max.y, min.z ), new( max.x + 1, max.y, min.z ) ), + new( new( min.x - 1, max.y, max.z ), new( max.x + 1, max.y, max.z ) ), + + new( new( min.x, min.y - 1, min.z ), new( min.x, max.y + 1, min.z ) ), + new( new( min.x, min.y - 1, max.z ), new( min.x, max.y + 1, max.z ) ), + new( new( max.x, min.y - 1, min.z ), new( max.x, max.y + 1, min.z ) ), + new( new( max.x, min.y - 1, max.z ), new( max.x, max.y + 1, max.z ) ), + + new( new( min.x, min.y, min.z - 1 ), new( min.x, min.y, max.z + 1 ) ), + new( new( min.x, max.y, min.z - 1 ), new( min.x, max.y, max.z + 1 ) ), + new( new( max.x, min.y, min.z - 1 ), new( max.x, min.y, max.z + 1 ) ), + new( new( max.x, max.y, min.z - 1 ), new( max.x, max.y, max.z + 1 ) ), + }; + + var indices = Mesh.TriangulatePolygon( CollectionsMarshal.AsSpan( vertices ) ); + + for ( var i = 0; i < gridEdges.Length; i++ ) + { + var e = gridEdges[i]; + if ( PolygonIntersectLineSegment( vertices, indices, e.Start, e.End, out var hit ) ) + snaps.Add( new SnapPoint( hit, EdgeCutAlignment.None ) ); + } + } + + void GenerateSnapPoints( MeshFace face, Vector3 target, out List snapPoints ) + { + var alignedOnly = _cutPoints.Count > 0 && Gizmo.IsShiftPressed; + + var all = new List(); + GenerateVertexSnapPoints( face, all ); + GenerateEdgeSnapPoints( face, target, all ); + + if ( _cutPoints.Count > 0 ) + GenerateFaceSnapPoints( face, target, all ); + + snapPoints = new List( all.Count ); + + const float minDistSq = 0.01f * 0.01f; + + foreach ( var snap in all ) + { + if ( alignedOnly && snap.Alignment == EdgeCutAlignment.None ) + continue; + + var tooClose = false; + for ( var i = 0; i < snapPoints.Count; i++ ) + { + if ( snapPoints[i].Position.DistanceSquared( snap.Position ) < minDistSq ) + { + tooClose = true; + break; + } + } + + if ( !tooClose ) + snapPoints.Add( snap ); + } + } + + static bool ComputePerpendicularPlaneNormalForCutPoint( MeshCutPoint cutPoint, out Vector3 planeNormal ) + { + planeNormal = default; + + if ( !cutPoint.Component.IsValid() || !cutPoint.Face.IsValid() || !cutPoint.Edge.IsValid() ) + return false; + + var mesh = cutPoint.Component.Mesh; + mesh.GetVerticesConnectedToEdge( cutPoint.Edge.Handle, cutPoint.Face.Handle, out var a, out var b ); + mesh.GetVertexPosition( a, Transform.Zero, out var pa ); + mesh.GetVertexPosition( b, Transform.Zero, out var pb ); + + planeNormal = (pb - pa).Normal; + return true; + } + + static bool SnapToEdge( Vector3 original, Vector3 a, Vector3 b, float maxDistance, bool gridSnap, out Vector3 snapped ) + { + ClosestPointOnLineSegment( original, a, b, out var pointOnLine ); + + var maxDistSq = maxDistance < float.MaxValue ? maxDistance * maxDistance : float.MaxValue; + var snappedPoint = Vector3.Zero; + var snappedOk = false; + + if ( pointOnLine.DistanceSquared( original ) < maxDistSq ) + { + if ( gridSnap ) + { + if ( IntersectRayWithGrid( pointOnLine, a, out var snapA ) && + snapA.DistanceSquared( original ) < maxDistSq ) + { + snappedPoint = snapA; + snappedOk = true; + } + + if ( IntersectRayWithGrid( pointOnLine, b, out var snapB ) && + snapB.DistanceSquared( original ) < maxDistSq && + pointOnLine.DistanceSquared( snapB ) < pointOnLine.DistanceSquared( snappedPoint ) ) + { + snappedPoint = snapB; + snappedOk = true; + } + } + else + { + snappedPoint = pointOnLine; + snappedOk = true; + } + } + + snapped = snappedOk ? snappedPoint : Vector3.Zero; + return snappedOk; + } + + static float DistanceSqrToLine( Vector3 p, Vector3 a, Vector3 b, out float t ) + { + ClosestPointOnLine( p, a, b, out var closest, out t ); + return p.DistanceSquared( closest ); + } + + static bool ComputePlaneIntersectionSnapPoint( Vector3 vPlaneNormal, Vector3 vPlanePoint, Vector3 vOrginalPoint, Vector3 vEdgePointA, Vector3 vEdgePointB, out Vector3 pOutIntersectionPoint ) + { + pOutIntersectionPoint = Vector3.Zero; + + const float halfGridSize = 0.125f * 0.5f; + + var vDelta = (vEdgePointA - vEdgePointB) * vPlaneNormal; + + if ( vDelta.Length >= halfGridSize ) + { + var plane = new Plane( vPlaneNormal, vPlaneNormal.Dot( vPlanePoint ) ); + var vIntersection = plane.IntersectLine( vEdgePointA, vEdgePointB ); + if ( vIntersection.HasValue ) + { + pOutIntersectionPoint = vIntersection.Value; + return true; + } + } + + return false; + } + + static bool InsideTriangle( Vector3 a, Vector3 b, Vector3 c, Vector3 p, Vector3 normal ) + { + const float eps = 1e-7f; + const float maxEdgeDistSq = eps * eps; + + if ( Vector3.Dot( Vector3.Cross( b - a, p - a ), normal ) >= -eps && + Vector3.Dot( Vector3.Cross( c - b, p - b ), normal ) >= -eps && + Vector3.Dot( Vector3.Cross( a - c, p - c ), normal ) >= -eps + ) + return true; + + if ( DistanceSqrToLine( p, a, b, out var t ) < maxEdgeDistSq && t is >= 0f and <= 1f ) return true; + if ( DistanceSqrToLine( p, b, c, out t ) < maxEdgeDistSq && t is >= 0f and <= 1f ) return true; + if ( DistanceSqrToLine( p, c, a, out t ) < maxEdgeDistSq && t is >= 0f and <= 1f ) return true; + + return false; + } + + static bool PolygonIntersectLineSegment( List vertices, Span indices, Vector3 a, Vector3 b, out Vector3 intersection ) + { + intersection = Vector3.Zero; + + for ( var i = 0; i + 2 < indices.Length; i += 3 ) + { + var v0 = vertices[indices[i]]; + var v1 = vertices[indices[i + 1]]; + var v2 = vertices[indices[i + 2]]; + + var normal = Vector3.Cross( v1 - v0, v2 - v0 ); + if ( normal.LengthSquared < 1e-8f ) continue; + + normal = normal.Normal; + var plane = new Plane( v0, normal ); + + var hit = plane.IntersectLine( a, b ); + if ( !hit.HasValue ) continue; + + var p = hit.Value; + if ( InsideTriangle( v0, v1, v2, p, normal ) ) + { + intersection = p; + return true; + } + } + + return false; + } + + static void ClosestPointOnLineSegment( Vector3 p, Vector3 a, Vector3 b, out Vector3 closest ) + { + var d = b - a; + var len2 = d.Dot( d ); + var t = len2 < 1e-5f ? 0f : Math.Clamp( d.Dot( p - a ) / len2, 0f, 1f ); + closest = a + d * t; + } + + static bool IntersectRayWithGrid( Vector3 origin, Vector3 end, out Vector3 intersection ) + { + var spacing = Gizmo.Settings.GridSpacing; + var dir = end - origin; + + var cell = new Vector3( + MathF.Floor( origin.x / spacing ) * spacing, + MathF.Floor( origin.y / spacing ) * spacing, + MathF.Floor( origin.z / spacing ) * spacing + ); + + var hit = false; + var closestT = 1f; + + for ( var axis = 0; axis < 3; axis++ ) + { + var d = dir[axis]; + if ( d == 0f ) + continue; + + var sign = d > 0f ? 1f : -1f; + var plane = cell[axis] + (sign > 0f ? spacing : 0f); + + var t = (plane - origin[axis]) / d; + if ( t > 0f && t <= closestT ) + { + closestT = t; + hit = true; + } + } + + intersection = hit ? origin + dir * closestT : Vector3.Zero; + return hit; + } +} diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.UI.cs b/game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.UI.cs new file mode 100644 index 00000000..0e6498dd --- /dev/null +++ b/game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.UI.cs @@ -0,0 +1,45 @@ + +namespace Editor.MeshEditor; + +partial class EdgeCutTool +{ + public override Widget CreateToolSidebar() + { + return new EdgeCutToolWidget( this ); + } + + public class EdgeCutToolWidget : ToolSidebarWidget + { + readonly EdgeCutTool _tool; + + public EdgeCutToolWidget( EdgeCutTool tool ) : base() + { + _tool = tool; + + AddTitle( "Edge Cut Tool", "content_cut" ); + + { + var row = Layout.AddRow(); + row.Spacing = 4; + + var apply = new Button( "Apply", "done" ); + apply.Clicked = Apply; + apply.ToolTip = "[Apply " + EditorShortcuts.GetKeys( "mesh.edge-cut-apply" ) + "]"; + row.Add( apply ); + + var cancel = new Button( "Cancel", "close" ); + cancel.Clicked = Cancel; + cancel.ToolTip = "[Cancel " + EditorShortcuts.GetKeys( "mesh.edge-cut-cancel" ) + "]"; + row.Add( cancel ); + } + + Layout.AddStretchCell(); + } + + [Shortcut( "mesh.edge-cut-apply", "enter", typeof( SceneDock ) )] + void Apply() => _tool.Apply(); + + [Shortcut( "mesh.edge-cut-cancel", "ESC", typeof( SceneDock ) )] + void Cancel() => _tool.Cancel(); + } +} diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.cs b/game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.cs new file mode 100644 index 00000000..13b973e5 --- /dev/null +++ b/game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.cs @@ -0,0 +1,174 @@ +using HalfEdgeMesh; + +namespace Editor.MeshEditor; + +[Alias( "tools.edge-cut-tool" )] +public partial class EdgeCutTool( string tool ) : EditorTool +{ + static int SelectionSampleRadius => 8; + + MeshComponent _hoveredMesh; + MeshCutPoint _previewCutPoint; + readonly List _cutPoints = []; + + struct MeshCutPoint : IValid + { + public MeshFace Face; + public MeshVertex Vertex; + public MeshEdge Edge; + public Vector3 WorldPosition; + public Vector3 LocalPosition; + public Vector3 BasePosition; + public MeshComponent Component; + + public readonly bool IsValid => Face.IsValid(); + + void SetWorldPosition( Vector3 position ) + { + WorldPosition = position; + LocalPosition = Face.Component.WorldTransform.PointToLocal( position ); + + if ( Vertex.IsValid() ) + { + BasePosition = Vertex.PositionLocal; + } + else if ( Edge.IsValid() ) + { + var mesh = Edge.Component.Mesh; + mesh.GetVerticesConnectedToEdge( Edge.Handle, out var hVertexA, out var hVertexB ); + mesh.ComputeClosestPointOnEdge( hVertexA, hVertexB, LocalPosition, out var flBaseParam ); + mesh.GetVertexPosition( hVertexA, Transform.Zero, out var vPositionA ); + mesh.GetVertexPosition( hVertexB, Transform.Zero, out var vPositionB ); + BasePosition = vPositionA.LerpTo( vPositionB, flBaseParam ); + } + else + { + BasePosition = LocalPosition; + } + } + + public MeshCutPoint( MeshFace face, MeshVertex vertex ) + { + Face = face; + Vertex = vertex; + Component = Face.Component; + SetWorldPosition( vertex.PositionWorld ); + } + + public MeshCutPoint( MeshFace face, MeshEdge edge, Vector3 point ) + { + Face = face; + Edge = edge; + Component = Face.Component; + SetWorldPosition( point ); + } + + public MeshCutPoint( MeshFace face, Vector3 point ) + { + Face = face; + Component = Face.Component; + SetWorldPosition( point ); + } + + public readonly void GetConnectedFaces( out List outFaces ) + { + outFaces = []; + + if ( Component.IsValid() == false ) return; + + var mesh = Component.Mesh; + + if ( Vertex.IsValid() ) + { + mesh.GetFacesConnectedToVertex( Vertex.Handle, out outFaces ); + } + else if ( Edge.IsValid() ) + { + mesh.GetFacesConnectedToEdge( Edge.Handle, out var hFaceA, out var hFaceB ); + outFaces.Add( hFaceA ); + outFaces.Add( hFaceB ); + } + else + { + outFaces.Add( Face.Handle ); + } + } + } + + readonly string _tool = tool; + bool _cancel; + + public override void OnDisabled() + { + Cancel(); + } + + public override void OnUpdate() + { + var escape = Application.IsKeyDown( KeyCode.Escape ); + if ( escape && !_cancel ) Cancel(); + _cancel = escape; + + _previewCutPoint = ShouldSnap() ? FindSnappedCutPoint() : FindCutPoint(); + + MeshCutPoint previousCutPoint = default; + if ( _cutPoints.Count > 0 ) + { + previousCutPoint = _cutPoints.Last(); + } + + if ( previousCutPoint.IsValid() ) + { + var sharedFace = FindSharedFace( previousCutPoint, _previewCutPoint ); + if ( sharedFace.IsValid() == false ) + { + _previewCutPoint = default; + } + } + + if ( !Gizmo.Pressed.Any ) + { + if ( Gizmo.WasLeftMousePressed ) + { + PlaceCutPoint(); + } + } + + DrawCutPoints(); + DrawPreview(); + DrawMesh( _hoveredMesh ); + } + + void PlaceCutPoint() + { + if ( _previewCutPoint.IsValid() == false ) return; + + _cutPoints.Add( _previewCutPoint ); + } + + MeshCutPoint FindCutPoint() + { + var trace = MeshTrace; + MeshCutPoint newCutPoint = default; + { + var vertex = trace.GetClosestVertex( SelectionSampleRadius, out var face ); + if ( vertex.IsValid() ) newCutPoint = new MeshCutPoint( face, vertex ); + } + + if ( newCutPoint.IsValid() == false ) + { + var edge = trace.GetClosestEdge( SelectionSampleRadius, out var face, out var hitPosition ); + var transform = edge.Transform; + var pointOnEdge = edge.Line.ClosestPoint( transform.PointToLocal( hitPosition ) ); + if ( edge.IsValid() ) newCutPoint = new MeshCutPoint( face, edge, transform.PointToWorld( pointOnEdge ) ); + } + + if ( _cutPoints.Count > 0 && newCutPoint.IsValid() == false ) + { + var face = trace.TraceFace( out var hitPosition ); + if ( face.IsValid() ) newCutPoint = new MeshCutPoint( face, hitPosition ); + } + + return newCutPoint; + } +} diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/EdgeTool.UI.cs b/game/addons/tools/Code/Scene/Mesh/Tools/EdgeTool.UI.cs index 3cc7a982..e1ff5ec2 100644 --- a/game/addons/tools/Code/Scene/Mesh/Tools/EdgeTool.UI.cs +++ b/game/addons/tools/Code/Scene/Mesh/Tools/EdgeTool.UI.cs @@ -43,7 +43,6 @@ partial class EdgeTool CreateButton( "Dissolve", "blur_off", "mesh.dissolve", Dissolve, CanDissolve(), row.Layout ); CreateButton( "Collapse", "unfold_less", "mesh.collapse", Collapse, CanCollapse(), row.Layout ); - CreateButton( "Bevel", "straighten", "mesh.edge-bevel", Bevel, CanBevel(), row.Layout ); CreateButton( "Connect", "link", "mesh.connect", Connect, CanConnect(), row.Layout ); CreateButton( "Extend", "call_made", "mesh.extend", Extend, CanExtend(), row.Layout ); @@ -107,9 +106,31 @@ partial class EdgeTool group.Add( row ); } + { + var group = AddGroup( "Tools" ); + + var grid = Layout.Row(); + grid.Spacing = 4; + + CreateButton( "Bevel", "straighten", "mesh.edge-bevel", Bevel, CanBevel(), grid ); + CreateButton( "Edge Cut Tool", "content_cut", "mesh.edge-cut-tool", OpenEdgeCutTool, true, grid ); + + grid.AddStretchCell(); + + group.Add( grid ); + } + Layout.AddStretchCell(); } + [Shortcut( "mesh.edge-cut-tool", "C", typeof( SceneDock ) )] + void OpenEdgeCutTool() + { + var tool = new EdgeCutTool( nameof( EdgeTool ) ); + tool.Manager = _tool.Manager; + _tool.CurrentTool = tool; + } + private void SetNormals( PolygonMesh.EdgeSmoothMode mode ) { using ( SceneEditorSession.Active.UndoScope( "Set Normals" ) diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/FaceTool.UI.cs b/game/addons/tools/Code/Scene/Mesh/Tools/FaceTool.UI.cs index 8e6cb926..6c12875f 100644 --- a/game/addons/tools/Code/Scene/Mesh/Tools/FaceTool.UI.cs +++ b/game/addons/tools/Code/Scene/Mesh/Tools/FaceTool.UI.cs @@ -73,9 +73,31 @@ partial class FaceTool group.Add( grid ); } + { + var group = AddGroup( "Tools" ); + + var grid = Layout.Row(); + grid.Spacing = 4; + + CreateButton( "Fast Texture Tool", "texture", "mesh.fast-texture-tool", OpenFastTextureTool, true, grid ); + CreateButton( "Edge Cut Tool", "content_cut", "mesh.edge-cut-tool", OpenEdgeCutTool, true, grid ); + + grid.AddStretchCell(); + + group.Add( grid ); + } + Layout.AddStretchCell(); } + [Shortcut( "mesh.edge-cut-tool", "C", typeof( SceneDock ) )] + void OpenEdgeCutTool() + { + var tool = new EdgeCutTool( nameof( FaceTool ) ); + tool.Manager = _meshTool.Manager; + _meshTool.CurrentTool = tool; + } + [Shortcut( "mesh.fast-texture-tool", "CTRL+G", typeof( SceneDock ) )] public void OpenFastTextureTool() { diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/SceneTraceExtensions.cs b/game/addons/tools/Code/Scene/Mesh/Tools/SceneTraceExtensions.cs index 24386d26..1dc1b89a 100644 --- a/game/addons/tools/Code/Scene/Mesh/Tools/SceneTraceExtensions.cs +++ b/game/addons/tools/Code/Scene/Mesh/Tools/SceneTraceExtensions.cs @@ -5,10 +5,12 @@ static class SceneTraceMeshExtensions { static Vector2 RayScreenPosition => SceneViewportWidget.MousePosition; - public static MeshVertex GetClosestVertex( this SceneTrace trace, int radius ) + public static MeshVertex GetClosestVertex( this SceneTrace trace, int radius ) => GetClosestVertex( trace, radius, out _ ); + + public static MeshVertex GetClosestVertex( this SceneTrace trace, int radius, out MeshFace bestFace ) { var point = RayScreenPosition; - var bestFace = TraceFace( trace, out var bestHitDistance ); + bestFace = TraceFace( trace, out var bestHitDistance, out _ ); var bestVertex = bestFace.GetClosestVertex( point, radius ); if ( bestFace.IsValid() && bestVertex.IsValid() ) @@ -34,11 +36,13 @@ static class SceneTraceMeshExtensions return bestVertex; } - public static MeshEdge GetClosestEdge( this SceneTrace trace, int radius ) + public static MeshEdge GetClosestEdge( this SceneTrace trace, int radius ) => GetClosestEdge( trace, radius, out _, out _ ); + + public static MeshEdge GetClosestEdge( this SceneTrace trace, int radius, out MeshFace bestFace, out Vector3 hitPosition ) { var point = RayScreenPosition; - var bestFace = TraceFace( trace, out var bestHitDistance ); - var hitPosition = Gizmo.CurrentRay.Project( bestHitDistance ); + bestFace = TraceFace( trace, out var bestHitDistance, out _ ); + hitPosition = Gizmo.CurrentRay.Project( bestHitDistance ); var bestEdge = bestFace.GetClosestEdge( hitPosition, point, radius ); if ( bestFace.IsValid() && bestEdge.IsValid() ) @@ -66,14 +70,26 @@ static class SceneTraceMeshExtensions return bestEdge; } - static MeshFace TraceFace( this SceneTrace trace, out float distance ) + public static MeshFace TraceFace( this SceneTrace trace ) + { + return TraceFace( trace, out _, out _ ); + } + + public static MeshFace TraceFace( this SceneTrace trace, out Vector3 hitPosition ) + { + return TraceFace( trace, out _, out hitPosition ); + } + + static MeshFace TraceFace( this SceneTrace trace, out float distance, out Vector3 hitPosition ) { distance = default; + hitPosition = default; var result = trace.Run(); if ( !result.Hit || result.Component is not MeshComponent component ) return default; + hitPosition = result.HitPosition; distance = result.Distance; var face = component.Mesh.TriangleToFace( result.Triangle ); return new MeshFace( component, face ); @@ -115,4 +131,40 @@ static class SceneTraceMeshExtensions return faces; } + + public static MeshFace TraceFace( this SceneTrace trace, int radius, out Vector3 hitPosition ) + { + MeshFace closest = default; + var closestDist = float.MaxValue; + var closestPoint = Vector3.Zero; + var point = RayScreenPosition; + + void TestRay( Ray ray ) + { + var result = trace.Ray( ray, Gizmo.RayDepth ).Run(); + if ( !result.Hit || result.Distance >= closestDist ) + return; + + if ( result.Component is not MeshComponent component ) + return; + + closest = new MeshFace( component, component.Mesh.TriangleToFace( result.Triangle ) ); + closestDist = result.Distance; + closestPoint = result.HitPosition; + } + + TestRay( Gizmo.CurrentRay ); + + for ( var ring = 1; ring < radius; ring++ ) + { + TestRay( Gizmo.Camera.GetRay( point + new Vector2( 0, ring ) ) ); + TestRay( Gizmo.Camera.GetRay( point + new Vector2( ring, 0 ) ) ); + TestRay( Gizmo.Camera.GetRay( point + new Vector2( 0, -ring ) ) ); + TestRay( Gizmo.Camera.GetRay( point + new Vector2( -ring, 0 ) ) ); + } + + hitPosition = closestPoint; + + return closest; + } } diff --git a/game/addons/tools/Code/Scene/Mesh/Tools/VertexTool.UI.cs b/game/addons/tools/Code/Scene/Mesh/Tools/VertexTool.UI.cs index e58facba..96f84af2 100644 --- a/game/addons/tools/Code/Scene/Mesh/Tools/VertexTool.UI.cs +++ b/game/addons/tools/Code/Scene/Mesh/Tools/VertexTool.UI.cs @@ -14,6 +14,7 @@ partial class VertexTool private readonly MeshVertex[] _vertices; private readonly List> _vertexGroups; private readonly List _components; + readonly MeshTool _tool; public enum MergeRange { @@ -39,6 +40,8 @@ partial class VertexTool public VertexSelectionWidget( SerializedObject so, MeshTool tool ) : base() { + _tool = tool; + AddTitle( "Vertex Mode", "workspaces" ); { @@ -86,6 +89,7 @@ partial class VertexTool CreateButton( "Weld UVs", "scatter_plot", "mesh.vertex-weld-uvs", WeldUVs, _vertices.Length > 0, row.Layout ); CreateButton( "Bevel", "straighten", "mesh.bevel", Bevel, _vertices.Length > 0, row.Layout ); CreateButton( "Connect", "link", "mesh.connect", Connect, _vertices.Length > 1, row.Layout ); + CreateButton( "Edge Cut Tool", "content_cut", "mesh.edge-cut-tool", OpenEdgeCutTool, true, row.Layout ); row.Layout.AddStretchCell(); @@ -96,6 +100,14 @@ partial class VertexTool Layout.AddStretchCell(); } + [Shortcut( "mesh.edge-cut-tool", "C", typeof( SceneDock ) )] + void OpenEdgeCutTool() + { + var tool = new EdgeCutTool( nameof( VertexTool ) ); + tool.Manager = _tool.Manager; + _tool.CurrentTool = tool; + } + [Shortcut( "mesh.connect", "V", typeof( SceneDock ) )] private void Connect() {