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 );
|
||||
}
|
||||
|
||||
private bool AreEdgesCoLinear( HalfEdgeHandle hEdgeA, HalfEdgeHandle hEdgeB, float flAngleToleranceInDegrees )
|
||||
public bool AreEdgesCoLinear( HalfEdgeHandle hEdgeA, HalfEdgeHandle hEdgeB, float flAngleToleranceInDegrees )
|
||||
{
|
||||
float flTolerance = MathF.Cos( MathF.Min( flAngleToleranceInDegrees, 180.0f ).DegreeToRadian() );
|
||||
|
||||
@@ -653,6 +653,13 @@ public sealed partial class PolygonMesh : IJsonConvert
|
||||
IsDirty = true;
|
||||
}
|
||||
|
||||
public void DissolveEdge( HalfEdgeHandle edge )
|
||||
{
|
||||
Topology.DissolveEdge( edge, out _ );
|
||||
|
||||
IsDirty = true;
|
||||
}
|
||||
|
||||
public void DissolveEdges( IReadOnlyList<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
|
||||
@@ -727,6 +734,68 @@ public sealed partial class PolygonMesh : IJsonConvert
|
||||
IsDirty = true;
|
||||
}
|
||||
|
||||
public bool ComputeClosestPointOnEdge( VertexHandle hVertexA, VertexHandle hVertexB, Vector3 vTargetPoint, out float pOutBaseEdgeParam )
|
||||
{
|
||||
pOutBaseEdgeParam = 0.0f;
|
||||
var hEdge = FindEdgeConnectingVertices( hVertexA, hVertexB );
|
||||
if ( hEdge == HalfEdgeHandle.Invalid )
|
||||
return false;
|
||||
|
||||
var vEdgePositions = new Vector3[2];
|
||||
vEdgePositions[0] = GetVertexPosition( hVertexA );
|
||||
vEdgePositions[1] = GetVertexPosition( hVertexB );
|
||||
int nNumPositions = vEdgePositions.Length;
|
||||
|
||||
float flEdgeLength = 0.0f;
|
||||
for ( int iPos = 1; iPos < nNumPositions; ++iPos )
|
||||
{
|
||||
flEdgeLength += vEdgePositions[iPos - 1].Distance( vEdgePositions[iPos] );
|
||||
}
|
||||
|
||||
Vector3 vClosestPointOnEdge = Vector3.Zero;
|
||||
float flClosestPointParam = 0.0f;
|
||||
float flMinDistanceSqr = float.MaxValue;
|
||||
float flClosestSegmentParam = 0.0f;
|
||||
float flBaseEdgeParam = 0.0f;
|
||||
int nClosestSegment = -1;
|
||||
|
||||
float flSegmentStart = 0.0f;
|
||||
for ( int iPos = 1; iPos < nNumPositions; ++iPos )
|
||||
{
|
||||
var vEdgePosA = vEdgePositions[iPos - 1];
|
||||
var vEdgePosB = vEdgePositions[iPos];
|
||||
|
||||
var vSegment = vEdgePosB - vEdgePosA;
|
||||
float flSegmentLength = vSegment.Length;
|
||||
float flSegmentEnd = flSegmentStart + flSegmentLength;
|
||||
|
||||
CalcClosestPointOnLineSegment( vTargetPoint, vEdgePosA, vEdgePosB, out var vClosestPointOnSegment, out var flSegmentParam );
|
||||
|
||||
float flDistSqr = vClosestPointOnSegment.DistanceSquared( vTargetPoint );
|
||||
if ( flDistSqr < flMinDistanceSqr )
|
||||
{
|
||||
flMinDistanceSqr = flDistSqr;
|
||||
vClosestPointOnEdge = vClosestPointOnSegment;
|
||||
flClosestSegmentParam = flSegmentParam;
|
||||
nClosestSegment = iPos - 1;
|
||||
float flPointDistance = flSegmentStart + flSegmentParam * flSegmentLength;
|
||||
flClosestPointParam = flPointDistance / flEdgeLength;
|
||||
|
||||
flBaseEdgeParam = MathX.Lerp( (iPos - 1) / (float)(nNumPositions - 1), iPos / (float)(nNumPositions - 1), flSegmentParam );
|
||||
}
|
||||
|
||||
flSegmentStart = flSegmentEnd;
|
||||
}
|
||||
|
||||
Assert.True( nClosestSegment >= 0 );
|
||||
if ( nClosestSegment < 0 )
|
||||
return false;
|
||||
|
||||
pOutBaseEdgeParam = flBaseEdgeParam;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RemoveVerticesFromColinearEdgesInFace( FaceHandle hFace, float flColinearAngleTolerance )
|
||||
{
|
||||
// Get all of the vertices in the face
|
||||
@@ -743,23 +812,192 @@ public sealed partial class PolygonMesh : IJsonConvert
|
||||
}
|
||||
}
|
||||
|
||||
private bool RemoveColinearVertex( VertexHandle hVertex, float flColinearAngleTolerance )
|
||||
bool RemoveColinearVertex( VertexHandle hVertex, float flColinearAngleTolerance )
|
||||
{
|
||||
return RemoveColinearVertexAndUpdateTable( hVertex, null, flColinearAngleTolerance );
|
||||
}
|
||||
|
||||
public bool RemoveColinearVertexAndUpdateTable( VertexHandle hVertex, SortedSet<HalfEdgeHandle> edgeTable, float flColinearAngleTolerance = 5.0f )
|
||||
{
|
||||
Topology.GetFullEdgesConnectedToVertex( hVertex, out var edgesConnectedToVertex );
|
||||
|
||||
if ( edgesConnectedToVertex.Count == 2 )
|
||||
if ( edgesConnectedToVertex is not null && edgesConnectedToVertex.Count == 2 )
|
||||
{
|
||||
if ( AreEdgesCoLinear( edgesConnectedToVertex[0], edgesConnectedToVertex[1], flColinearAngleTolerance ) )
|
||||
{
|
||||
// Were either of the edges in the table
|
||||
bool bEdgeInTable = false;
|
||||
if ( edgeTable is not null )
|
||||
{
|
||||
if ( edgeTable.Contains( edgesConnectedToVertex[0] ) ||
|
||||
edgeTable.Contains( edgesConnectedToVertex[1] ) )
|
||||
{
|
||||
bEdgeInTable = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the vertices at the ends of each edge opposite the vertex about to be removed.
|
||||
GetVerticesConnectedToEdge( edgesConnectedToVertex[0], out var hVertexA, out var hVertexB );
|
||||
var hVertex0 = (hVertexA == hVertex) ? hVertexB : hVertexA;
|
||||
|
||||
GetVerticesConnectedToEdge( edgesConnectedToVertex[1], out hVertexA, out hVertexB );
|
||||
var hVertex1 = (hVertexA == hVertex) ? hVertexB : hVertexA;
|
||||
|
||||
// Remove the vertex, combining the two edges into a single edge
|
||||
if ( Topology.RemoveVertex( hVertex, true ) )
|
||||
{
|
||||
// Remove the two old edges from the table and add the new edge
|
||||
if ( bEdgeInTable )
|
||||
{
|
||||
edgeTable.Remove( edgesConnectedToVertex[0] );
|
||||
edgeTable.Remove( edgesConnectedToVertex[1] );
|
||||
var hCombinedEdge = FindEdgeConnectingVertices( hVertex0, hVertex1 );
|
||||
if ( hCombinedEdge != HalfEdgeHandle.Invalid )
|
||||
{
|
||||
edgeTable.Add( hCombinedEdge );
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool GetEdgesConnectedToVertex( VertexHandle hVertex, out List<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
|
||||
{
|
||||
None, // Never remove vertices
|
||||
@@ -1396,6 +1634,43 @@ public sealed partial class PolygonMesh : IJsonConvert
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool AddVertexToEdgeAndUpdateTable( VertexHandle hVertexA, VertexHandle hVertexB, float flParam, out VertexHandle pNewVertex, SortedSet<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 )
|
||||
{
|
||||
return Topology.RemoveVertex( hVertex, removeFreeVerts );
|
||||
@@ -2386,6 +2661,8 @@ public sealed partial class PolygonMesh : IJsonConvert
|
||||
TextureCoord[hFaceVertex] = texCoord;
|
||||
}
|
||||
}
|
||||
|
||||
IsDirty = true;
|
||||
}
|
||||
|
||||
private static bool CalcTextureBasisFromUVs( Vector3[] vVertPos, Vector2[] vTexCoord, out Vector3 vOutU, out Vector3 vOutV )
|
||||
@@ -3039,7 +3316,7 @@ public sealed partial class PolygonMesh : IJsonConvert
|
||||
return Topology.GetFullEdgesConnectedToFace( hFace, out edges );
|
||||
}
|
||||
|
||||
private bool GetVerticesConnectedToEdge( HalfEdgeHandle hEdge, FaceHandle hFace, out VertexHandle hOutVertexA, out VertexHandle hOutVertexB )
|
||||
public bool GetVerticesConnectedToEdge( HalfEdgeHandle hEdge, FaceHandle hFace, out VertexHandle hOutVertexA, out VertexHandle hOutVertexB )
|
||||
{
|
||||
hOutVertexA = VertexHandle.Invalid;
|
||||
hOutVertexB = VertexHandle.Invalid;
|
||||
@@ -3114,7 +3391,7 @@ public sealed partial class PolygonMesh : IJsonConvert
|
||||
return HalfEdgeHandle.Invalid;
|
||||
}
|
||||
|
||||
private bool GetVerticesConnectedToFace( FaceHandle hFace, out VertexHandle[] vertices )
|
||||
public bool GetVerticesConnectedToFace( FaceHandle hFace, out VertexHandle[] vertices )
|
||||
{
|
||||
return Topology.GetVerticesConnectedToFace( hFace, out vertices );
|
||||
}
|
||||
@@ -4221,6 +4498,115 @@ public sealed partial class PolygonMesh : IJsonConvert
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
bool FindCutEdgeIntersection( VertexHandle hVertex, Vector3 targetPosition, out HalfEdgeHandle outEdge, out FaceHandle outFace, out Vector3 outPosition )
|
||||
{
|
||||
outEdge = HalfEdgeHandle.Invalid;
|
||||
outFace = FaceHandle.Invalid;
|
||||
outPosition = default;
|
||||
|
||||
GetVertexPosition( hVertex, Transform.Zero, out var vCurrentPosition );
|
||||
var vDir = (targetPosition - vCurrentPosition).Normal;
|
||||
|
||||
GetFacesConnectedToVertex( hVertex, out var connectedFaces );
|
||||
|
||||
var hBestFace = FaceHandle.Invalid;
|
||||
var hBestVertexA = VertexHandle.Invalid;
|
||||
var hBestVertexB = VertexHandle.Invalid;
|
||||
var vBestPoint = Vector3.Zero;
|
||||
var flMinDistance = float.MaxValue;
|
||||
|
||||
int nNumFaces = connectedFaces.Count;
|
||||
for ( int iFace = 0; iFace < nNumFaces; ++iFace )
|
||||
{
|
||||
var hFace = connectedFaces[iFace];
|
||||
|
||||
ComputeFaceNormal( hFace, out var vFaceNormal );
|
||||
|
||||
if ( MathF.Abs( vFaceNormal.Dot( vDir ) ) > 0.5f )
|
||||
continue;
|
||||
|
||||
var vCutPlaneNormal = vFaceNormal.Cross( vDir ).Normal;
|
||||
var cutPlane = new Plane( vCurrentPosition, vCutPlaneNormal );
|
||||
var basePlane = new Plane( vCurrentPosition, vDir );
|
||||
|
||||
var hStartFaceVertex = FindFaceVertexConnectedToVertex( hVertex, hFace );
|
||||
var hFaceVertexA = GetNextVertexInFace( hStartFaceVertex );
|
||||
var hFaceVertexB = GetNextVertexInFace( hFaceVertexA );
|
||||
|
||||
var hBestVertexForFaceA = VertexHandle.Invalid;
|
||||
var hBestVertexForFaceB = VertexHandle.Invalid;
|
||||
var vBestPointForFace = Vector3.Zero;
|
||||
var flMinBasePlaneDistance = float.MaxValue;
|
||||
|
||||
while ( hFaceVertexB != hStartFaceVertex )
|
||||
{
|
||||
var hVertexA = GetVertexConnectedToFaceVertex( hFaceVertexA );
|
||||
var hVertexB = GetVertexConnectedToFaceVertex( hFaceVertexB );
|
||||
|
||||
if ( (hVertexA != hVertex) && (hVertexB != hVertex) )
|
||||
{
|
||||
var vPositionA = GetVertexPosition( hVertexA );
|
||||
var vPositionB = GetVertexPosition( hVertexB );
|
||||
var vIntersection = cutPlane.IntersectLine( vPositionA, vPositionB );
|
||||
if ( vIntersection.HasValue )
|
||||
{
|
||||
float flBasePlaneDistance = basePlane.GetDistance( vIntersection.Value );
|
||||
|
||||
if ( (flBasePlaneDistance >= 0) && (flBasePlaneDistance <= flMinBasePlaneDistance) )
|
||||
{
|
||||
var vAB = vPositionB - vPositionA;
|
||||
var vCross = vDir.Cross( vAB );
|
||||
|
||||
if ( vCross.Dot( vFaceNormal ) > 0.0f )
|
||||
{
|
||||
hBestVertexForFaceA = hVertexA;
|
||||
hBestVertexForFaceB = hVertexB;
|
||||
vBestPointForFace = vIntersection.Value;
|
||||
}
|
||||
else if ( flBasePlaneDistance < flMinBasePlaneDistance )
|
||||
{
|
||||
hBestVertexForFaceA = VertexHandle.Invalid;
|
||||
hBestVertexForFaceB = VertexHandle.Invalid;
|
||||
vBestPointForFace = Vector3.Zero;
|
||||
}
|
||||
|
||||
flMinBasePlaneDistance = flBasePlaneDistance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hFaceVertexA = hFaceVertexB;
|
||||
hFaceVertexB = GetNextVertexInFace( hFaceVertexB );
|
||||
}
|
||||
|
||||
var flFaceTargetDistance = vBestPointForFace.Distance( targetPosition );
|
||||
if ( (hBestVertexForFaceA != VertexHandle.Invalid) &&
|
||||
(hBestVertexForFaceB != VertexHandle.Invalid) &&
|
||||
(flFaceTargetDistance < flMinDistance) )
|
||||
{
|
||||
hBestFace = hFace;
|
||||
hBestVertexA = hBestVertexForFaceA;
|
||||
hBestVertexB = hBestVertexForFaceB;
|
||||
vBestPoint = vBestPointForFace;
|
||||
flMinDistance = flFaceTargetDistance;
|
||||
}
|
||||
}
|
||||
|
||||
if ( hBestFace == FaceHandle.Invalid )
|
||||
return false;
|
||||
|
||||
outEdge = FindEdgeConnectingVertices( hBestVertexA, hBestVertexB );
|
||||
outFace = hBestFace;
|
||||
outPosition = vBestPoint;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void GetFacesConnectedToEdge( HalfEdgeHandle hEdge, out FaceHandle hOutFaceA, out FaceHandle hOutFaceB )
|
||||
{
|
||||
Topology.GetFacesConnectedToFullEdge( hEdge, out hOutFaceA, out hOutFaceB );
|
||||
}
|
||||
|
||||
private static readonly Vector3[] FaceNormals =
|
||||
{
|
||||
new( 0, 0, 1 ),
|
||||
|
||||
@@ -179,11 +179,21 @@ public struct Plane : System.IEquatable<Plane>
|
||||
float d1 = GetDistance( start );
|
||||
float d2 = GetDistance( end );
|
||||
|
||||
if ( MathF.Abs( d1 ) < 0.001f ) return start;
|
||||
if ( MathF.Abs( d1 - d2 ) < 0.001f ) return default;
|
||||
const float eps = 0.001f;
|
||||
|
||||
if ( MathF.Abs( d1 - d2 ) < eps )
|
||||
{
|
||||
if ( MathF.Abs( d1 ) < eps )
|
||||
return start;
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
float t = -d1 / (d2 - d1);
|
||||
return (t is >= 0.0f and <= 1.0f) ? start + (end - start) * t : default;
|
||||
if ( t >= 0.0f && t <= 1.0f )
|
||||
return start + (end - start) * t;
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <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( "Collapse", "unfold_less", "mesh.collapse", Collapse, CanCollapse(), row.Layout );
|
||||
CreateButton( "Bevel", "straighten", "mesh.edge-bevel", Bevel, CanBevel(), row.Layout );
|
||||
CreateButton( "Connect", "link", "mesh.connect", Connect, CanConnect(), row.Layout );
|
||||
CreateButton( "Extend", "call_made", "mesh.extend", Extend, CanExtend(), row.Layout );
|
||||
|
||||
@@ -107,9 +106,31 @@ partial class EdgeTool
|
||||
group.Add( row );
|
||||
}
|
||||
|
||||
{
|
||||
var group = AddGroup( "Tools" );
|
||||
|
||||
var grid = Layout.Row();
|
||||
grid.Spacing = 4;
|
||||
|
||||
CreateButton( "Bevel", "straighten", "mesh.edge-bevel", Bevel, CanBevel(), grid );
|
||||
CreateButton( "Edge Cut Tool", "content_cut", "mesh.edge-cut-tool", OpenEdgeCutTool, true, grid );
|
||||
|
||||
grid.AddStretchCell();
|
||||
|
||||
group.Add( grid );
|
||||
}
|
||||
|
||||
Layout.AddStretchCell();
|
||||
}
|
||||
|
||||
[Shortcut( "mesh.edge-cut-tool", "C", typeof( SceneDock ) )]
|
||||
void OpenEdgeCutTool()
|
||||
{
|
||||
var tool = new EdgeCutTool( nameof( EdgeTool ) );
|
||||
tool.Manager = _tool.Manager;
|
||||
_tool.CurrentTool = tool;
|
||||
}
|
||||
|
||||
private void SetNormals( PolygonMesh.EdgeSmoothMode mode )
|
||||
{
|
||||
using ( SceneEditorSession.Active.UndoScope( "Set Normals" )
|
||||
|
||||
@@ -73,9 +73,31 @@ partial class FaceTool
|
||||
group.Add( grid );
|
||||
}
|
||||
|
||||
{
|
||||
var group = AddGroup( "Tools" );
|
||||
|
||||
var grid = Layout.Row();
|
||||
grid.Spacing = 4;
|
||||
|
||||
CreateButton( "Fast Texture Tool", "texture", "mesh.fast-texture-tool", OpenFastTextureTool, true, grid );
|
||||
CreateButton( "Edge Cut Tool", "content_cut", "mesh.edge-cut-tool", OpenEdgeCutTool, true, grid );
|
||||
|
||||
grid.AddStretchCell();
|
||||
|
||||
group.Add( grid );
|
||||
}
|
||||
|
||||
Layout.AddStretchCell();
|
||||
}
|
||||
|
||||
[Shortcut( "mesh.edge-cut-tool", "C", typeof( SceneDock ) )]
|
||||
void OpenEdgeCutTool()
|
||||
{
|
||||
var tool = new EdgeCutTool( nameof( FaceTool ) );
|
||||
tool.Manager = _meshTool.Manager;
|
||||
_meshTool.CurrentTool = tool;
|
||||
}
|
||||
|
||||
[Shortcut( "mesh.fast-texture-tool", "CTRL+G", typeof( SceneDock ) )]
|
||||
public void OpenFastTextureTool()
|
||||
{
|
||||
|
||||
@@ -5,10 +5,12 @@ static class SceneTraceMeshExtensions
|
||||
{
|
||||
static Vector2 RayScreenPosition => SceneViewportWidget.MousePosition;
|
||||
|
||||
public static MeshVertex GetClosestVertex( this SceneTrace trace, int radius )
|
||||
public static MeshVertex GetClosestVertex( this SceneTrace trace, int radius ) => GetClosestVertex( trace, radius, out _ );
|
||||
|
||||
public static MeshVertex GetClosestVertex( this SceneTrace trace, int radius, out MeshFace bestFace )
|
||||
{
|
||||
var point = RayScreenPosition;
|
||||
var bestFace = TraceFace( trace, out var bestHitDistance );
|
||||
bestFace = TraceFace( trace, out var bestHitDistance, out _ );
|
||||
var bestVertex = bestFace.GetClosestVertex( point, radius );
|
||||
|
||||
if ( bestFace.IsValid() && bestVertex.IsValid() )
|
||||
@@ -34,11 +36,13 @@ static class SceneTraceMeshExtensions
|
||||
return bestVertex;
|
||||
}
|
||||
|
||||
public static MeshEdge GetClosestEdge( this SceneTrace trace, int radius )
|
||||
public static MeshEdge GetClosestEdge( this SceneTrace trace, int radius ) => GetClosestEdge( trace, radius, out _, out _ );
|
||||
|
||||
public static MeshEdge GetClosestEdge( this SceneTrace trace, int radius, out MeshFace bestFace, out Vector3 hitPosition )
|
||||
{
|
||||
var point = RayScreenPosition;
|
||||
var bestFace = TraceFace( trace, out var bestHitDistance );
|
||||
var hitPosition = Gizmo.CurrentRay.Project( bestHitDistance );
|
||||
bestFace = TraceFace( trace, out var bestHitDistance, out _ );
|
||||
hitPosition = Gizmo.CurrentRay.Project( bestHitDistance );
|
||||
var bestEdge = bestFace.GetClosestEdge( hitPosition, point, radius );
|
||||
|
||||
if ( bestFace.IsValid() && bestEdge.IsValid() )
|
||||
@@ -66,14 +70,26 @@ static class SceneTraceMeshExtensions
|
||||
return bestEdge;
|
||||
}
|
||||
|
||||
static MeshFace TraceFace( this SceneTrace trace, out float distance )
|
||||
public static MeshFace TraceFace( this SceneTrace trace )
|
||||
{
|
||||
return TraceFace( trace, out _, out _ );
|
||||
}
|
||||
|
||||
public static MeshFace TraceFace( this SceneTrace trace, out Vector3 hitPosition )
|
||||
{
|
||||
return TraceFace( trace, out _, out hitPosition );
|
||||
}
|
||||
|
||||
static MeshFace TraceFace( this SceneTrace trace, out float distance, out Vector3 hitPosition )
|
||||
{
|
||||
distance = default;
|
||||
hitPosition = default;
|
||||
|
||||
var result = trace.Run();
|
||||
if ( !result.Hit || result.Component is not MeshComponent component )
|
||||
return default;
|
||||
|
||||
hitPosition = result.HitPosition;
|
||||
distance = result.Distance;
|
||||
var face = component.Mesh.TriangleToFace( result.Triangle );
|
||||
return new MeshFace( component, face );
|
||||
@@ -115,4 +131,40 @@ static class SceneTraceMeshExtensions
|
||||
|
||||
return faces;
|
||||
}
|
||||
|
||||
public static MeshFace TraceFace( this SceneTrace trace, int radius, out Vector3 hitPosition )
|
||||
{
|
||||
MeshFace closest = default;
|
||||
var closestDist = float.MaxValue;
|
||||
var closestPoint = Vector3.Zero;
|
||||
var point = RayScreenPosition;
|
||||
|
||||
void TestRay( Ray ray )
|
||||
{
|
||||
var result = trace.Ray( ray, Gizmo.RayDepth ).Run();
|
||||
if ( !result.Hit || result.Distance >= closestDist )
|
||||
return;
|
||||
|
||||
if ( result.Component is not MeshComponent component )
|
||||
return;
|
||||
|
||||
closest = new MeshFace( component, component.Mesh.TriangleToFace( result.Triangle ) );
|
||||
closestDist = result.Distance;
|
||||
closestPoint = result.HitPosition;
|
||||
}
|
||||
|
||||
TestRay( Gizmo.CurrentRay );
|
||||
|
||||
for ( var ring = 1; ring < radius; ring++ )
|
||||
{
|
||||
TestRay( Gizmo.Camera.GetRay( point + new Vector2( 0, ring ) ) );
|
||||
TestRay( Gizmo.Camera.GetRay( point + new Vector2( ring, 0 ) ) );
|
||||
TestRay( Gizmo.Camera.GetRay( point + new Vector2( 0, -ring ) ) );
|
||||
TestRay( Gizmo.Camera.GetRay( point + new Vector2( -ring, 0 ) ) );
|
||||
}
|
||||
|
||||
hitPosition = closestPoint;
|
||||
|
||||
return closest;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ partial class VertexTool
|
||||
private readonly MeshVertex[] _vertices;
|
||||
private readonly List<IGrouping<MeshComponent, MeshVertex>> _vertexGroups;
|
||||
private readonly List<MeshComponent> _components;
|
||||
readonly MeshTool _tool;
|
||||
|
||||
public enum MergeRange
|
||||
{
|
||||
@@ -39,6 +40,8 @@ partial class VertexTool
|
||||
|
||||
public VertexSelectionWidget( SerializedObject so, MeshTool tool ) : base()
|
||||
{
|
||||
_tool = tool;
|
||||
|
||||
AddTitle( "Vertex Mode", "workspaces" );
|
||||
|
||||
{
|
||||
@@ -86,6 +89,7 @@ partial class VertexTool
|
||||
CreateButton( "Weld UVs", "scatter_plot", "mesh.vertex-weld-uvs", WeldUVs, _vertices.Length > 0, row.Layout );
|
||||
CreateButton( "Bevel", "straighten", "mesh.bevel", Bevel, _vertices.Length > 0, row.Layout );
|
||||
CreateButton( "Connect", "link", "mesh.connect", Connect, _vertices.Length > 1, row.Layout );
|
||||
CreateButton( "Edge Cut Tool", "content_cut", "mesh.edge-cut-tool", OpenEdgeCutTool, true, row.Layout );
|
||||
|
||||
row.Layout.AddStretchCell();
|
||||
|
||||
@@ -96,6 +100,14 @@ partial class VertexTool
|
||||
Layout.AddStretchCell();
|
||||
}
|
||||
|
||||
[Shortcut( "mesh.edge-cut-tool", "C", typeof( SceneDock ) )]
|
||||
void OpenEdgeCutTool()
|
||||
{
|
||||
var tool = new EdgeCutTool( nameof( VertexTool ) );
|
||||
tool.Manager = _tool.Manager;
|
||||
_tool.CurrentTool = tool;
|
||||
}
|
||||
|
||||
[Shortcut( "mesh.connect", "V", typeof( SceneDock ) )]
|
||||
private void Connect()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user