1+ using DeveMazeGeneratorCore . Coaster3MF . Models ;
2+
3+ namespace DeveMazeGeneratorCore . Coaster3MF
4+ {
5+ /// <summary>
6+ /// Detects non-manifold edges and other mesh topology issues in 3D meshes.
7+ /// Non-manifold edges are edges that are shared by more than 2 triangles or have inconsistent winding.
8+ /// </summary>
9+ public class NonManifoldEdgeDetector
10+ {
11+ /// <summary>
12+ /// Represents an edge between two vertices.
13+ /// </summary>
14+ public record Edge ( int V1 , int V2 )
15+ {
16+ /// <summary>
17+ /// Creates a normalized edge where V1 <= V2 for consistent comparison.
18+ /// </summary>
19+ public static Edge CreateNormalized ( int v1 , int v2 )
20+ {
21+ return v1 <= v2 ? new Edge ( v1 , v2 ) : new Edge ( v2 , v1 ) ;
22+ }
23+ }
24+
25+ /// <summary>
26+ /// Represents an edge with direction (for winding order checks).
27+ /// </summary>
28+ public record DirectedEdge ( int From , int To ) ;
29+
30+ /// <summary>
31+ /// Results of non-manifold edge detection.
32+ /// </summary>
33+ public class DetectionResult
34+ {
35+ public bool IsManifold => ! HasNonManifoldEdges && ! HasInconsistentWinding && ! HasDuplicateVertices ;
36+
37+ public bool HasNonManifoldEdges { get ; set ; }
38+ public bool HasInconsistentWinding { get ; set ; }
39+ public bool HasDuplicateVertices { get ; set ; }
40+
41+ public List < Edge > NonManifoldEdges { get ; } = new ( ) ;
42+ public List < Edge > BorderEdges { get ; } = new ( ) ;
43+ public List < DirectedEdge > InconsistentEdges { get ; } = new ( ) ;
44+ public List < ( int Index1 , int Index2 ) > DuplicateVertices { get ; } = new ( ) ;
45+
46+ public Dictionary < Edge , int > EdgeTriangleCounts { get ; } = new ( ) ;
47+ }
48+
49+ /// <summary>
50+ /// Analyzes a mesh for non-manifold edges and other topology issues.
51+ /// </summary>
52+ /// <param name="meshData">The mesh to analyze</param>
53+ /// <returns>Detection results containing all found issues</returns>
54+ public DetectionResult AnalyzeMesh ( MeshData meshData )
55+ {
56+ var result = new DetectionResult ( ) ;
57+
58+ // Detect duplicate vertices
59+ DetectDuplicateVertices ( meshData , result ) ;
60+
61+ // Analyze edge topology
62+ AnalyzeEdgeTopology ( meshData , result ) ;
63+
64+ // Check winding consistency
65+ CheckWindingConsistency ( meshData , result ) ;
66+
67+ return result ;
68+ }
69+
70+ /// <summary>
71+ /// Detects vertices that are at the same position (within tolerance).
72+ /// </summary>
73+ private void DetectDuplicateVertices ( MeshData meshData , DetectionResult result )
74+ {
75+ const float tolerance = 1e-6f ;
76+
77+ for ( int i = 0 ; i < meshData . Vertices . Count ; i ++ )
78+ {
79+ for ( int j = i + 1 ; j < meshData . Vertices . Count ; j ++ )
80+ {
81+ var v1 = meshData . Vertices [ i ] ;
82+ var v2 = meshData . Vertices [ j ] ;
83+
84+ var dx = v1 . X - v2 . X ;
85+ var dy = v1 . Y - v2 . Y ;
86+ var dz = v1 . Z - v2 . Z ;
87+ var distanceSquared = dx * dx + dy * dy + dz * dz ;
88+
89+ if ( distanceSquared < tolerance * tolerance )
90+ {
91+ result . DuplicateVertices . Add ( ( i , j ) ) ;
92+ result . HasDuplicateVertices = true ;
93+ }
94+ }
95+ }
96+ }
97+
98+ /// <summary>
99+ /// Analyzes edge topology to find non-manifold edges and border edges.
100+ /// </summary>
101+ private void AnalyzeEdgeTopology ( MeshData meshData , DetectionResult result )
102+ {
103+ var edgeCounts = new Dictionary < Edge , int > ( ) ;
104+
105+ // Count how many triangles share each edge
106+ foreach ( var triangle in meshData . Triangles )
107+ {
108+ var edges = new [ ]
109+ {
110+ Edge . CreateNormalized ( triangle . V1 , triangle . V2 ) ,
111+ Edge . CreateNormalized ( triangle . V2 , triangle . V3 ) ,
112+ Edge . CreateNormalized ( triangle . V3 , triangle . V1 )
113+ } ;
114+
115+ foreach ( var edge in edges )
116+ {
117+ edgeCounts [ edge ] = edgeCounts . GetValueOrDefault ( edge , 0 ) + 1 ;
118+ }
119+ }
120+
121+ // Classify edges based on triangle count
122+ foreach ( var ( edge , count ) in edgeCounts )
123+ {
124+ result . EdgeTriangleCounts [ edge ] = count ;
125+
126+ if ( count == 1 )
127+ {
128+ // Border edge (shared by only 1 triangle)
129+ result . BorderEdges . Add ( edge ) ;
130+ }
131+ else if ( count > 2 )
132+ {
133+ // Non-manifold edge (shared by more than 2 triangles)
134+ result . NonManifoldEdges . Add ( edge ) ;
135+ result . HasNonManifoldEdges = true ;
136+ }
137+ }
138+ }
139+
140+ /// <summary>
141+ /// Checks for consistent winding order across adjacent triangles.
142+ /// </summary>
143+ private void CheckWindingConsistency ( MeshData meshData , DetectionResult result )
144+ {
145+ var edgeToTriangles = new Dictionary < Edge , List < ( Triangle Triangle , bool IsForward ) > > ( ) ;
146+
147+ // Map each edge to the triangles that use it and track winding direction
148+ foreach ( var triangle in meshData . Triangles )
149+ {
150+ var edges = new [ ]
151+ {
152+ ( Edge . CreateNormalized ( triangle . V1 , triangle . V2 ) , triangle . V1 < triangle . V2 ) ,
153+ ( Edge . CreateNormalized ( triangle . V2 , triangle . V3 ) , triangle . V2 < triangle . V3 ) ,
154+ ( Edge . CreateNormalized ( triangle . V3 , triangle . V1 ) , triangle . V3 < triangle . V1 )
155+ } ;
156+
157+ foreach ( var ( edge , isForward ) in edges )
158+ {
159+ if ( ! edgeToTriangles . ContainsKey ( edge ) )
160+ edgeToTriangles [ edge ] = new List < ( Triangle , bool ) > ( ) ;
161+
162+ edgeToTriangles [ edge ] . Add ( ( triangle , isForward ) ) ;
163+ }
164+ }
165+
166+ // Check for inconsistent winding on edges shared by exactly 2 triangles
167+ foreach ( var ( edge , triangleInfos ) in edgeToTriangles )
168+ {
169+ if ( triangleInfos . Count == 2 )
170+ {
171+ var first = triangleInfos [ 0 ] ;
172+ var second = triangleInfos [ 1 ] ;
173+
174+ // Adjacent triangles should have opposite winding on shared edge
175+ if ( first . IsForward == second . IsForward )
176+ {
177+ result . InconsistentEdges . Add ( new DirectedEdge ( edge . V1 , edge . V2 ) ) ;
178+ result . HasInconsistentWinding = true ;
179+ }
180+ }
181+ }
182+ }
183+
184+ /// <summary>
185+ /// Creates a simple test mesh with the specified topology issues for testing.
186+ /// </summary>
187+ public static MeshData CreateTestMesh ( string meshType )
188+ {
189+ var meshData = new MeshData ( ) ;
190+
191+ switch ( meshType . ToLowerInvariant ( ) )
192+ {
193+ case "validcube" :
194+ CreateValidCube ( meshData ) ;
195+ break ;
196+
197+ case "nonmanifoldy" :
198+ CreateNonManifoldYMesh ( meshData ) ;
199+ break ;
200+
201+ case "borderhole" :
202+ CreateMeshWithHole ( meshData ) ;
203+ break ;
204+
205+ case "inconsistentwinding" :
206+ CreateInconsistentWindingMesh ( meshData ) ;
207+ break ;
208+
209+ case "duplicatevertices" :
210+ CreateDuplicateVerticesMesh ( meshData ) ;
211+ break ;
212+
213+ default :
214+ throw new ArgumentException ( $ "Unknown test mesh type: { meshType } ") ;
215+ }
216+
217+ return meshData ;
218+ }
219+
220+ private static void CreateValidCube ( MeshData meshData )
221+ {
222+ // Create a simple valid cube (manifold mesh)
223+ var vertices = new [ ]
224+ {
225+ new Vertex ( 0 , 0 , 0 ) , // 0
226+ new Vertex ( 1 , 0 , 0 ) , // 1
227+ new Vertex ( 1 , 1 , 0 ) , // 2
228+ new Vertex ( 0 , 1 , 0 ) , // 3
229+ new Vertex ( 0 , 0 , 1 ) , // 4
230+ new Vertex ( 1 , 0 , 1 ) , // 5
231+ new Vertex ( 1 , 1 , 1 ) , // 6
232+ new Vertex ( 0 , 1 , 1 ) // 7
233+ } ;
234+
235+ meshData . Vertices . AddRange ( vertices ) ;
236+
237+ // Define triangles with consistent winding (counter-clockwise when viewed from outside)
238+ var triangles = new [ ]
239+ {
240+ // Bottom face (z=0)
241+ new Triangle ( 0 , 2 , 1 , "" ) ,
242+ new Triangle ( 0 , 3 , 2 , "" ) ,
243+
244+ // Top face (z=1)
245+ new Triangle ( 4 , 5 , 6 , "" ) ,
246+ new Triangle ( 4 , 6 , 7 , "" ) ,
247+
248+ // Front face (y=0)
249+ new Triangle ( 0 , 1 , 5 , "" ) ,
250+ new Triangle ( 0 , 5 , 4 , "" ) ,
251+
252+ // Right face (x=1)
253+ new Triangle ( 1 , 2 , 6 , "" ) ,
254+ new Triangle ( 1 , 6 , 5 , "" ) ,
255+
256+ // Back face (y=1)
257+ new Triangle ( 2 , 3 , 7 , "" ) ,
258+ new Triangle ( 2 , 7 , 6 , "" ) ,
259+
260+ // Left face (x=0)
261+ new Triangle ( 3 , 0 , 4 , "" ) ,
262+ new Triangle ( 3 , 4 , 7 , "" )
263+ } ;
264+
265+ meshData . Triangles . AddRange ( triangles ) ;
266+ }
267+
268+ private static void CreateNonManifoldYMesh ( MeshData meshData )
269+ {
270+ // Create a "Y" shaped mesh where 3 triangles meet at a central edge (non-manifold)
271+ var vertices = new [ ]
272+ {
273+ new Vertex ( 0 , 0 , 0 ) , // 0
274+ new Vertex ( 1 , 0 , 0 ) , // 1 - central vertex
275+ new Vertex ( 2 , 0 , 0 ) , // 2
276+ new Vertex ( 0.5f , 1 , 0 ) , // 3
277+ new Vertex ( 1.5f , 1 , 0 ) , // 4
278+ new Vertex ( 1 , - 1 , 0 ) , // 5
279+ } ;
280+
281+ meshData . Vertices . AddRange ( vertices ) ;
282+
283+ // Three triangles sharing the edge (0,1) - this creates a non-manifold edge
284+ var triangles = new [ ]
285+ {
286+ new Triangle ( 0 , 1 , 3 , "" ) , // First triangle
287+ new Triangle ( 0 , 1 , 5 , "" ) , // Second triangle (shares edge 0-1)
288+ new Triangle ( 1 , 0 , 2 , "" ) , // Third triangle (shares edge 0-1, but with opposite direction)
289+ } ;
290+
291+ meshData . Triangles . AddRange ( triangles ) ;
292+ }
293+
294+ private static void CreateMeshWithHole ( MeshData meshData )
295+ {
296+ // Create a square with a triangular hole (border edges)
297+ var vertices = new [ ]
298+ {
299+ // Outer square
300+ new Vertex ( 0 , 0 , 0 ) , // 0
301+ new Vertex ( 2 , 0 , 0 ) , // 1
302+ new Vertex ( 2 , 2 , 0 ) , // 2
303+ new Vertex ( 0 , 2 , 0 ) , // 3
304+
305+ // Inner triangle (hole)
306+ new Vertex ( 0.5f , 0.5f , 0 ) , // 4
307+ new Vertex ( 1.5f , 0.5f , 0 ) , // 5
308+ new Vertex ( 1 , 1.5f , 0 ) , // 6
309+ } ;
310+
311+ meshData . Vertices . AddRange ( vertices ) ;
312+
313+ var triangles = new [ ]
314+ {
315+ // Outer triangles (with hole)
316+ new Triangle ( 0 , 1 , 4 , "" ) ,
317+ new Triangle ( 1 , 5 , 4 , "" ) ,
318+ new Triangle ( 1 , 2 , 5 , "" ) ,
319+ new Triangle ( 2 , 6 , 5 , "" ) ,
320+ new Triangle ( 2 , 3 , 6 , "" ) ,
321+ new Triangle ( 3 , 4 , 6 , "" ) ,
322+ new Triangle ( 3 , 0 , 4 , "" ) ,
323+
324+ // The hole creates border edges around triangle 4-5-6
325+ } ;
326+
327+ meshData . Triangles . AddRange ( triangles ) ;
328+ }
329+
330+ private static void CreateInconsistentWindingMesh ( MeshData meshData )
331+ {
332+ // Create two adjacent triangles with inconsistent winding
333+ var vertices = new [ ]
334+ {
335+ new Vertex ( 0 , 0 , 0 ) , // 0
336+ new Vertex ( 1 , 0 , 0 ) , // 1
337+ new Vertex ( 0.5f , 1 , 0 ) , // 2
338+ new Vertex ( 1.5f , 1 , 0 ) , // 3
339+ } ;
340+
341+ meshData . Vertices . AddRange ( vertices ) ;
342+
343+ var triangles = new [ ]
344+ {
345+ new Triangle ( 0 , 1 , 2 , "" ) , // Counter-clockwise: edges (0,1), (1,2), (2,0)
346+ new Triangle ( 1 , 2 , 3 , "" ) , // Counter-clockwise but same direction on shared edge (1,2) - this is inconsistent
347+ } ;
348+
349+ meshData . Triangles . AddRange ( triangles ) ;
350+ }
351+
352+ private static void CreateDuplicateVerticesMesh ( MeshData meshData )
353+ {
354+ // Create a mesh with duplicate vertices at the same position
355+ var vertices = new [ ]
356+ {
357+ new Vertex ( 0 , 0 , 0 ) , // 0
358+ new Vertex ( 1 , 0 , 0 ) , // 1
359+ new Vertex ( 0.5f , 1 , 0 ) , // 2
360+ new Vertex ( 0 , 0 , 0 ) , // 3 - duplicate of vertex 0
361+ } ;
362+
363+ meshData . Vertices . AddRange ( vertices ) ;
364+
365+ var triangles = new [ ]
366+ {
367+ new Triangle ( 0 , 1 , 2 , "" ) ,
368+ new Triangle ( 3 , 1 , 2 , "" ) , // Uses duplicate vertex
369+ } ;
370+
371+ meshData . Triangles . AddRange ( triangles ) ;
372+ }
373+ }
374+ }
0 commit comments