Skip to content

Commit 0035dfd

Browse files
authored
Merge pull request #96 from devedse/copilot/fix-95
Add NonManifoldEdgeDetector for 3MF mesh validation and analysis
2 parents 4ab19a5 + b19dd23 commit 0035dfd

File tree

2 files changed

+609
-0
lines changed

2 files changed

+609
-0
lines changed
Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
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

Comments
 (0)