Edge cut tool (#3629)

This commit is contained in:
Layla
2025-12-16 17:22:03 +00:00
committed by GitHub
parent c25d0aa594
commit f40e74a093
11 changed files with 1545 additions and 15 deletions

View File

@@ -620,7 +620,7 @@ public sealed partial class PolygonMesh : IJsonConvert
pOutVertexB = GetVertexPosition( hVertexB ); 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() ); float flTolerance = MathF.Cos( MathF.Min( flAngleToleranceInDegrees, 180.0f ).DegreeToRadian() );
@@ -653,6 +653,13 @@ public sealed partial class PolygonMesh : IJsonConvert
IsDirty = true; IsDirty = true;
} }
public void DissolveEdge( HalfEdgeHandle edge )
{
Topology.DissolveEdge( edge, out _ );
IsDirty = true;
}
public void DissolveEdges( IReadOnlyList<HalfEdgeHandle> edges, bool bFaceMustBePlanar, DissolveRemoveVertexCondition removeCondition ) public void DissolveEdges( IReadOnlyList<HalfEdgeHandle> 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 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; 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 ) private void RemoveVerticesFromColinearEdgesInFace( FaceHandle hFace, float flColinearAngleTolerance )
{ {
// Get all of the vertices in the face // 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<HalfEdgeHandle> edgeTable, float flColinearAngleTolerance = 5.0f )
{ {
Topology.GetFullEdgesConnectedToVertex( hVertex, out var edgesConnectedToVertex ); 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 ) ) 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 // Remove the vertex, combining the two edges into a single edge
if ( Topology.RemoveVertex( hVertex, true ) ) 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 true;
}
} }
} }
return false; return false;
} }
public bool GetEdgesConnectedToVertex( VertexHandle hVertex, out List<HalfEdgeHandle> 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<HalfEdgeHandle> pOutEdgeList, out bool pOutIsLastEdgeConnector, SortedSet<HalfEdgeHandle> 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 public enum DissolveRemoveVertexCondition
{ {
None, // Never remove vertices None, // Never remove vertices
@@ -1396,6 +1634,43 @@ public sealed partial class PolygonMesh : IJsonConvert
return true; return true;
} }
public bool AddVertexToEdgeAndUpdateTable( VertexHandle hVertexA, VertexHandle hVertexB, float flParam, out VertexHandle pNewVertex, SortedSet<HalfEdgeHandle> 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 ) public bool RemoveVertex( VertexHandle hVertex, bool removeFreeVerts )
{ {
return Topology.RemoveVertex( hVertex, removeFreeVerts ); return Topology.RemoveVertex( hVertex, removeFreeVerts );
@@ -2386,6 +2661,8 @@ public sealed partial class PolygonMesh : IJsonConvert
TextureCoord[hFaceVertex] = texCoord; TextureCoord[hFaceVertex] = texCoord;
} }
} }
IsDirty = true;
} }
private static bool CalcTextureBasisFromUVs( Vector3[] vVertPos, Vector2[] vTexCoord, out Vector3 vOutU, out Vector3 vOutV ) 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 ); 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; hOutVertexA = VertexHandle.Invalid;
hOutVertexB = VertexHandle.Invalid; hOutVertexB = VertexHandle.Invalid;
@@ -3114,7 +3391,7 @@ public sealed partial class PolygonMesh : IJsonConvert
return HalfEdgeHandle.Invalid; 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 ); return Topology.GetVerticesConnectedToFace( hFace, out vertices );
} }
@@ -4221,6 +4498,115 @@ public sealed partial class PolygonMesh : IJsonConvert
.ToArray(); .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 = private static readonly Vector3[] FaceNormals =
{ {
new( 0, 0, 1 ), new( 0, 0, 1 ),

View File

@@ -179,11 +179,21 @@ public struct Plane : System.IEquatable<Plane>
float d1 = GetDistance( start ); float d1 = GetDistance( start );
float d2 = GetDistance( end ); float d2 = GetDistance( end );
if ( MathF.Abs( d1 ) < 0.001f ) return start; const float eps = 0.001f;
if ( MathF.Abs( d1 - d2 ) < 0.001f ) return default;
if ( MathF.Abs( d1 - d2 ) < eps )
{
if ( MathF.Abs( d1 ) < eps )
return start;
return default;
}
float t = -d1 / (d2 - d1); 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;
} }
/// <summary> /// <summary>

View File

@@ -0,0 +1,293 @@
using HalfEdgeMesh;
using Sandbox.Diagnostics;
namespace Editor.MeshEditor;
partial class EdgeCutTool
{
sealed class HalfEdgeHandleComparer : IComparer<HalfEdgeHandle>
{
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<MeshComponent>( _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<MeshVertex>();
var edges = new List<MeshEdge>();
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<MeshVertex> outCutPathVertices, List<MeshEdge> outCutPathEdges )
{
var cutPoints = _cutPoints;
if ( cutPoints.Count == 0 ) return false;
var components = new List<MeshComponent>( cutPoints.Count );
var meshes = new List<PolygonMesh>( 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<SortedSet<HalfEdgeHandle>>( meshes.Count );
for ( int i = 0; i < meshes.Count; i++ ) edgeTables.Add( new SortedSet<HalfEdgeHandle>( 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<MeshVertex>( 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<HalfEdgeHandle> edgeTable )
{
if ( !edge.Component.IsValid() ) return VertexHandle.Invalid;
var mesh = edge.Component.Mesh;
var visited = new List<HalfEdgeHandle>( 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;
}
}

View File

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

View File

@@ -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<SnapPoint> 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<SnapPoint> 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<SnapPoint> 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<SnapPoint> 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<SnapPoint> snapPoints )
{
var alignedOnly = _cutPoints.Count > 0 && Gizmo.IsShiftPressed;
var all = new List<SnapPoint>();
GenerateVertexSnapPoints( face, all );
GenerateEdgeSnapPoints( face, target, all );
if ( _cutPoints.Count > 0 )
GenerateFaceSnapPoints( face, target, all );
snapPoints = new List<SnapPoint>( 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<Vector3> vertices, Span<int> 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;
}
}

View File

@@ -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();
}
}

View File

@@ -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<MeshCutPoint> _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<FaceHandle> 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;
}
}

View File

@@ -43,7 +43,6 @@ partial class EdgeTool
CreateButton( "Dissolve", "blur_off", "mesh.dissolve", Dissolve, CanDissolve(), row.Layout ); CreateButton( "Dissolve", "blur_off", "mesh.dissolve", Dissolve, CanDissolve(), row.Layout );
CreateButton( "Collapse", "unfold_less", "mesh.collapse", Collapse, CanCollapse(), 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( "Connect", "link", "mesh.connect", Connect, CanConnect(), row.Layout );
CreateButton( "Extend", "call_made", "mesh.extend", Extend, CanExtend(), row.Layout ); CreateButton( "Extend", "call_made", "mesh.extend", Extend, CanExtend(), row.Layout );
@@ -107,9 +106,31 @@ partial class EdgeTool
group.Add( row ); 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(); 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 ) private void SetNormals( PolygonMesh.EdgeSmoothMode mode )
{ {
using ( SceneEditorSession.Active.UndoScope( "Set Normals" ) using ( SceneEditorSession.Active.UndoScope( "Set Normals" )

View File

@@ -73,9 +73,31 @@ partial class FaceTool
group.Add( grid ); 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(); 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 ) )] [Shortcut( "mesh.fast-texture-tool", "CTRL+G", typeof( SceneDock ) )]
public void OpenFastTextureTool() public void OpenFastTextureTool()
{ {

View File

@@ -5,10 +5,12 @@ static class SceneTraceMeshExtensions
{ {
static Vector2 RayScreenPosition => SceneViewportWidget.MousePosition; 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 point = RayScreenPosition;
var bestFace = TraceFace( trace, out var bestHitDistance ); bestFace = TraceFace( trace, out var bestHitDistance, out _ );
var bestVertex = bestFace.GetClosestVertex( point, radius ); var bestVertex = bestFace.GetClosestVertex( point, radius );
if ( bestFace.IsValid() && bestVertex.IsValid() ) if ( bestFace.IsValid() && bestVertex.IsValid() )
@@ -34,11 +36,13 @@ static class SceneTraceMeshExtensions
return bestVertex; 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 point = RayScreenPosition;
var bestFace = TraceFace( trace, out var bestHitDistance ); bestFace = TraceFace( trace, out var bestHitDistance, out _ );
var hitPosition = Gizmo.CurrentRay.Project( bestHitDistance ); hitPosition = Gizmo.CurrentRay.Project( bestHitDistance );
var bestEdge = bestFace.GetClosestEdge( hitPosition, point, radius ); var bestEdge = bestFace.GetClosestEdge( hitPosition, point, radius );
if ( bestFace.IsValid() && bestEdge.IsValid() ) if ( bestFace.IsValid() && bestEdge.IsValid() )
@@ -66,14 +70,26 @@ static class SceneTraceMeshExtensions
return bestEdge; 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; distance = default;
hitPosition = default;
var result = trace.Run(); var result = trace.Run();
if ( !result.Hit || result.Component is not MeshComponent component ) if ( !result.Hit || result.Component is not MeshComponent component )
return default; return default;
hitPosition = result.HitPosition;
distance = result.Distance; distance = result.Distance;
var face = component.Mesh.TriangleToFace( result.Triangle ); var face = component.Mesh.TriangleToFace( result.Triangle );
return new MeshFace( component, face ); return new MeshFace( component, face );
@@ -115,4 +131,40 @@ static class SceneTraceMeshExtensions
return faces; 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;
}
} }

View File

@@ -14,6 +14,7 @@ partial class VertexTool
private readonly MeshVertex[] _vertices; private readonly MeshVertex[] _vertices;
private readonly List<IGrouping<MeshComponent, MeshVertex>> _vertexGroups; private readonly List<IGrouping<MeshComponent, MeshVertex>> _vertexGroups;
private readonly List<MeshComponent> _components; private readonly List<MeshComponent> _components;
readonly MeshTool _tool;
public enum MergeRange public enum MergeRange
{ {
@@ -39,6 +40,8 @@ partial class VertexTool
public VertexSelectionWidget( SerializedObject so, MeshTool tool ) : base() public VertexSelectionWidget( SerializedObject so, MeshTool tool ) : base()
{ {
_tool = tool;
AddTitle( "Vertex Mode", "workspaces" ); 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( "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( "Bevel", "straighten", "mesh.bevel", Bevel, _vertices.Length > 0, row.Layout );
CreateButton( "Connect", "link", "mesh.connect", Connect, _vertices.Length > 1, 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(); row.Layout.AddStretchCell();
@@ -96,6 +100,14 @@ partial class VertexTool
Layout.AddStretchCell(); 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 ) )] [Shortcut( "mesh.connect", "V", typeof( SceneDock ) )]
private void Connect() private void Connect()
{ {