diff --git a/src/wkb2gltf.core.tests/outlines/OutlineDetectionTests.cs b/src/wkb2gltf.core.tests/outlines/OutlineDetectionTests.cs index c4220cf5..11d1b3e7 100644 --- a/src/wkb2gltf.core.tests/outlines/OutlineDetectionTests.cs +++ b/src/wkb2gltf.core.tests/outlines/OutlineDetectionTests.cs @@ -197,4 +197,72 @@ public void FindConnectedTrianglesWithSameNormal() Assert.That(outlines[9] == 8, Is.True); } + [Test] + public void Issue200_CoplanarTrianglesNonSequential() + { + // Test for issue #200 from @bertt's comment + // Triangle 0 and 2 are coplanar and adjacent -> should NOT have outline between them + // Triangle 0 and 1 are not coplanar but adjacent -> SHOULD have outline between them + + // Triangle 0 (on z=5 plane, horizontal) + var t0 = new Triangle( + new Point(121346, 487295, 5), + new Point(121446, 487295, 5), + new Point(121396, 487381.603, 5), + 0 + ); + + // Triangle 1 (not coplanar with t0 or t2, forms a vertical/angled face) + var t1 = new Triangle( + new Point(121446, 487295, 5), + new Point(121396, 487381.603, 5), + new Point(121346, 487295, 100), + 0 + ); + + // Triangle 2 (on z=5 plane, horizontal, coplanar with t0) + var t2 = new Triangle( + new Point(121346, 487295, 5), + new Point(121446, 487295, 5), + new Point(121396, 487208.397, 5), + 0 + ); + + var triangles = new List { t0, t1, t2 }; + + // Verify coplanarity + Assert.That(t0.AreCoplanar(t2), Is.True, "t0 and t2 should be coplanar"); + Assert.That(t0.AreCoplanar(t1), Is.False, "t0 and t1 should NOT be coplanar"); + Assert.That(t1.AreCoplanar(t2), Is.False, "t1 and t2 should NOT be coplanar"); + + // Check the adjacency list + var adjacency = Adjacency.GetAdjacencyList(triangles); + + // t0 and t2 share edge (121346,487295,5)-(121446,487295,5) and are coplanar + // -> this edge SHOULD be in adjacency (to be excluded from outline) + // t0 and t1 share edge (121446,487295,5)-(121396,487381.603,5) and are NOT coplanar + // -> this edge should NOT be in adjacency (to be included in outline) + + Assert.That(adjacency.ContainsKey(0), Is.True, "t0 should have adjacency entries"); + Assert.That(adjacency.ContainsKey(2), Is.True, "t2 should have adjacency entries"); + + // t0 should have one adjacent edge (with t2, which is coplanar) + Assert.That(adjacency[0].Count, Is.EqualTo(1), "t0 should have 1 adjacent edge"); + + // t2 should have one adjacent edge (with t0, which is coplanar) + Assert.That(adjacency[2].Count, Is.EqualTo(1), "t2 should have 1 adjacent edge"); + + // t1 should NOT be in adjacency list (it's not coplanar with any other triangle) + Assert.That(adjacency.ContainsKey(1), Is.False, "t1 should NOT have adjacency entries (not coplanar with others)"); + + var outlines = OutlineDetection.GetOutlines2(triangles); + + // The outline should include: + // - All edges of t0 except the one shared with t2 + // - All edges of t1 (none are excluded) + // - All edges of t2 except the one shared with t0 + // That's 2 + 3 + 2 = 7 edges = 14 points + Assert.That(outlines.Count, Is.EqualTo(14), "Should have 7 edges (14 points) in outline"); + } + } diff --git a/src/wkb2gltf.core/Triangle.cs b/src/wkb2gltf.core/Triangle.cs index 6182c275..7b0bb408 100644 --- a/src/wkb2gltf.core/Triangle.cs +++ b/src/wkb2gltf.core/Triangle.cs @@ -69,4 +69,14 @@ public List GetPoints() return points; } + public bool AreCoplanar(Triangle other, double normalTolerance = 0.01) + { + var normal1 = GetNormal(); + var normal2 = other.GetNormal(); + var dotProduct = Vector3.Dot(normal1, normal2); + // Two triangles are coplanar if their normals point in the same direction (dot ≈ 1) + // or opposite directions (dot ≈ -1), indicating they lie on the same plane + return Math.Abs(dotProduct - 1.0f) <= normalTolerance || Math.Abs(dotProduct + 1.0f) <= normalTolerance; + } + } \ No newline at end of file diff --git a/src/wkb2gltf.core/outlines/Adjacency.cs b/src/wkb2gltf.core/outlines/Adjacency.cs index 71fc6672..b823d20a 100644 --- a/src/wkb2gltf.core/outlines/Adjacency.cs +++ b/src/wkb2gltf.core/outlines/Adjacency.cs @@ -5,9 +5,10 @@ namespace Wkb2Gltf.outlines; public static class Adjacency { /// - /// + /// Get the adjacency list for triangles. Only includes shared edges between COPLANAR triangles. + /// Shared edges between coplanar triangles are internal edges and should be excluded from outlines. /// - public static Dictionary> GetAdjacencyList(List triangles, double distanceTolerance = 0.01) + public static Dictionary> GetAdjacencyList(List triangles, double distanceTolerance = 0.01, double normalTolerance = 0.01) { var res = new Dictionary>(); @@ -18,8 +19,13 @@ public static class Adjacency if (i != j) { var boundaries = BoundaryDetection.GetSharedPoints(t0, triangles[j], distanceTolerance); if (boundaries.first.Count == 2 && boundaries.second.Count == 2) { - Upsert(res, i, boundaries.first[0], boundaries.first[1]); - Upsert(res, j, boundaries.second[0], boundaries.second[1]); + // Only add to adjacency list if triangles ARE coplanar + // Coplanar triangles sharing an edge means the edge is internal to a flat surface + // and should be excluded from the outline + if (t0.AreCoplanar(triangles[j], normalTolerance)) { + Upsert(res, i, boundaries.first[0], boundaries.first[1]); + Upsert(res, j, boundaries.second[0], boundaries.second[1]); + } } } } diff --git a/src/wkb2gltf.core/outlines/OutlineDetection.cs b/src/wkb2gltf.core/outlines/OutlineDetection.cs index 30f0ac5a..d8abaf6a 100644 --- a/src/wkb2gltf.core/outlines/OutlineDetection.cs +++ b/src/wkb2gltf.core/outlines/OutlineDetection.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using SharpGLTF.Geometry; using SharpGLTF.Schema2; @@ -32,11 +33,26 @@ public static List GetOutlines2(List triangles, double normalTol { var outlines = new List(); + + // Calculate global adjacency list BEFORE partitioning + // This allows coplanar triangles in different parts to exclude shared edges + var globalAdjacency = Adjacency.GetAdjacencyList(triangles, distanceTolerance, normalTolerance); + var parts = PartFinder.GetParts(triangles, normalTolerance, distanceTolerance); for (uint p = 0; p < parts.Count; p++) { var partTriangles = Triangles.SelectByIndex(triangles, parts[(int)p]); - var outline = Part.GetOutlines(partTriangles, parts[(int)p], 0, distanceTolerance); + + // Build a local adjacency list for this part by mapping global indices to local indices + var localAdjacency = new Dictionary>(); + for (var i = 0; i < partTriangles.Count; i++) { + var globalIndex = (int)parts[(int)p][i]; + if (globalAdjacency.ContainsKey(globalIndex)) { + localAdjacency[i] = globalAdjacency[globalIndex]; + } + } + + var outline = Part.GetOutlines(partTriangles, parts[(int)p], 0, distanceTolerance, normalTolerance, localAdjacency); outlines.AddRange(outline); } return outlines; diff --git a/src/wkb2gltf.core/outlines/Part.cs b/src/wkb2gltf.core/outlines/Part.cs index d4df58a3..f4c8cdf6 100644 --- a/src/wkb2gltf.core/outlines/Part.cs +++ b/src/wkb2gltf.core/outlines/Part.cs @@ -4,14 +4,22 @@ namespace Wkb2Gltf.outlines; public static class Part { - public static List GetOutlines(List triangles, List indices, uint offset = 0, double distanceTolerance = 0.01) + public static List GetOutlines(List triangles, List indices, uint offset = 0, double distanceTolerance = 0.01, double normalTolerance = 0.01, Dictionary> adjacency = null) { var result = new List(); if (triangles.Count == 1) { - result = GetOutlines(triangles[0], offset: offset + indices[0] * 3); + // Even for a single triangle, check if it has adjacency info (coplanar neighbors in other parts) + if (adjacency != null && adjacency.ContainsKey(0)) { + result = GetOutlines(triangles[0], adjacency[0], offset + indices[0] * 3); + } else { + result = GetOutlines(triangles[0], offset: offset + indices[0] * 3); + } } else if (triangles.Count > 1) { - var adjacency = Adjacency.GetAdjacencyList(triangles, distanceTolerance); + // Use provided adjacency list, or calculate it if not provided + if (adjacency == null) { + adjacency = Adjacency.GetAdjacencyList(triangles, distanceTolerance, normalTolerance); + } var i = 0; foreach (var triangle in triangles) { List<(int from, int to)> val;