mirror of
https://github.com/Facepunch/sbox-public.git
synced 2025-12-23 22:48:07 -05:00
Edge cut tool (#3629)
This commit is contained in:
@@ -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 ),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
293
game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.Apply.cs
Normal file
293
game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.Apply.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
118
game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.Gizmo.cs
Normal file
118
game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.Gizmo.cs
Normal 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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
397
game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.Snapping.cs
Normal file
397
game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.Snapping.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
45
game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.UI.cs
Normal file
45
game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.UI.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
174
game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.cs
Normal file
174
game/addons/tools/Code/Scene/Mesh/Tools/EdgeCutTool.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" )
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user