From dadb9737b3b2c89f308e8989d55f98f08caac4b7 Mon Sep 17 00:00:00 2001 From: bert Date: Fri, 30 Jan 2026 10:45:44 +0100 Subject: [PATCH 1/6] Fix #200: Improve outlines - prevent diagonal lines on coplanar polygons - Add AreCoplanar() method to Triangle class to check if two triangles share the same plane - Update BoundaryDetection.GetSharedPoints() to support optional coplanarity check - Enable coplanarity check in Adjacency.GetAdjacencyList() to exclude shared edges between coplanar triangles from outlines - Add comprehensive unit tests for coplanar and non-coplanar triangle scenarios - All 21 outline tests pass successfully This fix ensures that shared edges between triangles with the same orientation are properly excluded from the outline, eliminating unwanted diagonal lines on flat surfaces. --- .../outlines/BoundaryDetectionTests.cs | 49 +++++++++++++++++++ src/wkb2gltf.core/Triangle.cs | 8 +++ src/wkb2gltf.core/outlines/Adjacency.cs | 4 +- .../outlines/BoundaryDetection.cs | 10 +++- .../outlines/OutlineDetection.cs | 2 +- src/wkb2gltf.core/outlines/Part.cs | 4 +- 6 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/wkb2gltf.core.tests/outlines/BoundaryDetectionTests.cs b/src/wkb2gltf.core.tests/outlines/BoundaryDetectionTests.cs index 70ce039f..439b1995 100644 --- a/src/wkb2gltf.core.tests/outlines/BoundaryDetectionTests.cs +++ b/src/wkb2gltf.core.tests/outlines/BoundaryDetectionTests.cs @@ -20,4 +20,53 @@ public void TestSharedPoints() Assert.That(boundary.second[0] == 1, Is.True); Assert.That(boundary.second[1] == 0, Is.True); } + + [Test] + public void TestSharedPointsCoplanar() + { + // Two coplanar triangles on the same plane (z=0, normal pointing up) + var t0 = new Triangle(new Wkx.Point(0, 0, 0), new Wkx.Point(0, 1, 0), new Wkx.Point(1, 0, 0), 0); + var t1 = new Triangle(new Wkx.Point(1, 0, 0), new Wkx.Point(0, 1, 0), new Wkx.Point(1, 1, 0), 0); + + var boundary = BoundaryDetection.GetSharedPoints(t0, t1, checkCoplanar: true); + + // Should return shared points because triangles are coplanar + Assert.That(boundary.first.Count, Is.EqualTo(2)); + Assert.That(boundary.second.Count, Is.EqualTo(2)); + } + + [Test] + public void TestSharedPointsNonCoplanar() + { + // Two non-coplanar triangles with shared edge + // t0 is on z=0 plane (horizontal), t1 is vertical + var t0 = new Triangle(new Wkx.Point(0, 0, 0), new Wkx.Point(0, 1, 0), new Wkx.Point(1, 0, 0), 0); + var t1 = new Triangle(new Wkx.Point(0, 0, 0), new Wkx.Point(0, 1, 0), new Wkx.Point(0, 0.5, 1), 0); + + var boundary = BoundaryDetection.GetSharedPoints(t0, t1, checkCoplanar: true); + + // Should return empty because triangles are NOT coplanar + Assert.That(boundary.first.Count, Is.EqualTo(0)); + Assert.That(boundary.second.Count, Is.EqualTo(0)); + } + + [Test] + public void TestAreCoplanar() + { + // Two triangles on the same plane + var t0 = new Triangle(new Wkx.Point(0, 0, 0), new Wkx.Point(0, 1, 0), new Wkx.Point(1, 0, 0), 0); + var t1 = new Triangle(new Wkx.Point(1, 0, 0), new Wkx.Point(0, 1, 0), new Wkx.Point(1, 1, 0), 0); + + Assert.That(t0.AreCoplanar(t1), Is.True); + } + + [Test] + public void TestAreNotCoplanar() + { + // One horizontal, one vertical triangle + var t0 = new Triangle(new Wkx.Point(0, 0, 0), new Wkx.Point(0, 1, 0), new Wkx.Point(1, 0, 0), 0); + var t1 = new Triangle(new Wkx.Point(0, 0, 0), new Wkx.Point(0, 1, 0), new Wkx.Point(0, 0.5, 1), 0); + + Assert.That(t0.AreCoplanar(t1), Is.False); + } } diff --git a/src/wkb2gltf.core/Triangle.cs b/src/wkb2gltf.core/Triangle.cs index 6182c275..bc0eb9ff 100644 --- a/src/wkb2gltf.core/Triangle.cs +++ b/src/wkb2gltf.core/Triangle.cs @@ -69,4 +69,12 @@ 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); + return 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..0d913462 100644 --- a/src/wkb2gltf.core/outlines/Adjacency.cs +++ b/src/wkb2gltf.core/outlines/Adjacency.cs @@ -7,7 +7,7 @@ public static class Adjacency /// /// /// - 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>(); @@ -16,7 +16,7 @@ public static class Adjacency for (var j = 0; j < triangles.Count; j++) { if (i != j) { - var boundaries = BoundaryDetection.GetSharedPoints(t0, triangles[j], distanceTolerance); + var boundaries = BoundaryDetection.GetSharedPoints(t0, triangles[j], distanceTolerance, checkCoplanar: true, normalTolerance); 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]); diff --git a/src/wkb2gltf.core/outlines/BoundaryDetection.cs b/src/wkb2gltf.core/outlines/BoundaryDetection.cs index 0d369549..11625946 100644 --- a/src/wkb2gltf.core/outlines/BoundaryDetection.cs +++ b/src/wkb2gltf.core/outlines/BoundaryDetection.cs @@ -14,7 +14,7 @@ public static class BoundaryDetection return null; } - public static (List first, List second) GetSharedPoints(Triangle triangle0, Triangle triangle1, double distanceTolerance = 0.01) + public static (List first, List second) GetSharedPoints(Triangle triangle0, Triangle triangle1, double distanceTolerance = 0.01, bool checkCoplanar = false, double normalTolerance = 0.01) { var t0 = new List(); var t1 = new List(); @@ -28,6 +28,14 @@ public static (List first, List second) GetSharedPoints(Triangle trian } } + // If we found shared points and coplanar check is enabled, + // only return them if triangles are coplanar + if (checkCoplanar && t0.Count > 0) { + if (!triangle0.AreCoplanar(triangle1, normalTolerance)) { + return (new List(), new List()); + } + } + return (t0, t1); } } diff --git a/src/wkb2gltf.core/outlines/OutlineDetection.cs b/src/wkb2gltf.core/outlines/OutlineDetection.cs index 30f0ac5a..24f87b79 100644 --- a/src/wkb2gltf.core/outlines/OutlineDetection.cs +++ b/src/wkb2gltf.core/outlines/OutlineDetection.cs @@ -36,7 +36,7 @@ public static List GetOutlines2(List triangles, double normalTol 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); + var outline = Part.GetOutlines(partTriangles, parts[(int)p], 0, distanceTolerance, normalTolerance); outlines.AddRange(outline); } return outlines; diff --git a/src/wkb2gltf.core/outlines/Part.cs b/src/wkb2gltf.core/outlines/Part.cs index d4df58a3..605f0636 100644 --- a/src/wkb2gltf.core/outlines/Part.cs +++ b/src/wkb2gltf.core/outlines/Part.cs @@ -4,14 +4,14 @@ 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) { var result = new List(); if (triangles.Count == 1) { result = GetOutlines(triangles[0], offset: offset + indices[0] * 3); } else if (triangles.Count > 1) { - var adjacency = Adjacency.GetAdjacencyList(triangles, distanceTolerance); + var adjacency = Adjacency.GetAdjacencyList(triangles, distanceTolerance, normalTolerance); var i = 0; foreach (var triangle in triangles) { List<(int from, int to)> val; From 8c0f543a8af59d6546f200662e9be3179463ffe5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:00:43 +0000 Subject: [PATCH 2/6] Initial plan From 6544743617de2a24dfdef15f8f3891c794277fd1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:05:50 +0000 Subject: [PATCH 3/6] Revert incorrect coplanar fix - remove AreCoplanar method and related changes Co-authored-by: bertt <538812+bertt@users.noreply.github.com> --- .../outlines/BoundaryDetectionTests.cs | 49 ------------------- src/wkb2gltf.core/Triangle.cs | 8 --- src/wkb2gltf.core/outlines/Adjacency.cs | 4 +- .../outlines/BoundaryDetection.cs | 10 +--- src/wkb2gltf.core/outlines/Part.cs | 2 +- 5 files changed, 4 insertions(+), 69 deletions(-) diff --git a/src/wkb2gltf.core.tests/outlines/BoundaryDetectionTests.cs b/src/wkb2gltf.core.tests/outlines/BoundaryDetectionTests.cs index 439b1995..70ce039f 100644 --- a/src/wkb2gltf.core.tests/outlines/BoundaryDetectionTests.cs +++ b/src/wkb2gltf.core.tests/outlines/BoundaryDetectionTests.cs @@ -20,53 +20,4 @@ public void TestSharedPoints() Assert.That(boundary.second[0] == 1, Is.True); Assert.That(boundary.second[1] == 0, Is.True); } - - [Test] - public void TestSharedPointsCoplanar() - { - // Two coplanar triangles on the same plane (z=0, normal pointing up) - var t0 = new Triangle(new Wkx.Point(0, 0, 0), new Wkx.Point(0, 1, 0), new Wkx.Point(1, 0, 0), 0); - var t1 = new Triangle(new Wkx.Point(1, 0, 0), new Wkx.Point(0, 1, 0), new Wkx.Point(1, 1, 0), 0); - - var boundary = BoundaryDetection.GetSharedPoints(t0, t1, checkCoplanar: true); - - // Should return shared points because triangles are coplanar - Assert.That(boundary.first.Count, Is.EqualTo(2)); - Assert.That(boundary.second.Count, Is.EqualTo(2)); - } - - [Test] - public void TestSharedPointsNonCoplanar() - { - // Two non-coplanar triangles with shared edge - // t0 is on z=0 plane (horizontal), t1 is vertical - var t0 = new Triangle(new Wkx.Point(0, 0, 0), new Wkx.Point(0, 1, 0), new Wkx.Point(1, 0, 0), 0); - var t1 = new Triangle(new Wkx.Point(0, 0, 0), new Wkx.Point(0, 1, 0), new Wkx.Point(0, 0.5, 1), 0); - - var boundary = BoundaryDetection.GetSharedPoints(t0, t1, checkCoplanar: true); - - // Should return empty because triangles are NOT coplanar - Assert.That(boundary.first.Count, Is.EqualTo(0)); - Assert.That(boundary.second.Count, Is.EqualTo(0)); - } - - [Test] - public void TestAreCoplanar() - { - // Two triangles on the same plane - var t0 = new Triangle(new Wkx.Point(0, 0, 0), new Wkx.Point(0, 1, 0), new Wkx.Point(1, 0, 0), 0); - var t1 = new Triangle(new Wkx.Point(1, 0, 0), new Wkx.Point(0, 1, 0), new Wkx.Point(1, 1, 0), 0); - - Assert.That(t0.AreCoplanar(t1), Is.True); - } - - [Test] - public void TestAreNotCoplanar() - { - // One horizontal, one vertical triangle - var t0 = new Triangle(new Wkx.Point(0, 0, 0), new Wkx.Point(0, 1, 0), new Wkx.Point(1, 0, 0), 0); - var t1 = new Triangle(new Wkx.Point(0, 0, 0), new Wkx.Point(0, 1, 0), new Wkx.Point(0, 0.5, 1), 0); - - Assert.That(t0.AreCoplanar(t1), Is.False); - } } diff --git a/src/wkb2gltf.core/Triangle.cs b/src/wkb2gltf.core/Triangle.cs index bc0eb9ff..6182c275 100644 --- a/src/wkb2gltf.core/Triangle.cs +++ b/src/wkb2gltf.core/Triangle.cs @@ -69,12 +69,4 @@ 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); - return 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 0d913462..71fc6672 100644 --- a/src/wkb2gltf.core/outlines/Adjacency.cs +++ b/src/wkb2gltf.core/outlines/Adjacency.cs @@ -7,7 +7,7 @@ public static class Adjacency /// /// /// - public static Dictionary> GetAdjacencyList(List triangles, double distanceTolerance = 0.01, double normalTolerance = 0.01) + public static Dictionary> GetAdjacencyList(List triangles, double distanceTolerance = 0.01) { var res = new Dictionary>(); @@ -16,7 +16,7 @@ public static class Adjacency for (var j = 0; j < triangles.Count; j++) { if (i != j) { - var boundaries = BoundaryDetection.GetSharedPoints(t0, triangles[j], distanceTolerance, checkCoplanar: true, normalTolerance); + 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]); diff --git a/src/wkb2gltf.core/outlines/BoundaryDetection.cs b/src/wkb2gltf.core/outlines/BoundaryDetection.cs index 11625946..0d369549 100644 --- a/src/wkb2gltf.core/outlines/BoundaryDetection.cs +++ b/src/wkb2gltf.core/outlines/BoundaryDetection.cs @@ -14,7 +14,7 @@ public static class BoundaryDetection return null; } - public static (List first, List second) GetSharedPoints(Triangle triangle0, Triangle triangle1, double distanceTolerance = 0.01, bool checkCoplanar = false, double normalTolerance = 0.01) + public static (List first, List second) GetSharedPoints(Triangle triangle0, Triangle triangle1, double distanceTolerance = 0.01) { var t0 = new List(); var t1 = new List(); @@ -28,14 +28,6 @@ public static (List first, List second) GetSharedPoints(Triangle trian } } - // If we found shared points and coplanar check is enabled, - // only return them if triangles are coplanar - if (checkCoplanar && t0.Count > 0) { - if (!triangle0.AreCoplanar(triangle1, normalTolerance)) { - return (new List(), new List()); - } - } - return (t0, t1); } } diff --git a/src/wkb2gltf.core/outlines/Part.cs b/src/wkb2gltf.core/outlines/Part.cs index 605f0636..952cd78f 100644 --- a/src/wkb2gltf.core/outlines/Part.cs +++ b/src/wkb2gltf.core/outlines/Part.cs @@ -11,7 +11,7 @@ public static List GetOutlines(List triangles, List indice result = GetOutlines(triangles[0], offset: offset + indices[0] * 3); } else if (triangles.Count > 1) { - var adjacency = Adjacency.GetAdjacencyList(triangles, distanceTolerance, normalTolerance); + var adjacency = Adjacency.GetAdjacencyList(triangles, distanceTolerance); var i = 0; foreach (var triangle in triangles) { List<(int from, int to)> val; From a696406c1ca5bcac524725ff979493ac69342a06 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:14:05 +0000 Subject: [PATCH 4/6] Add coplanarity check to exclude internal edges - partial fix for #200 Co-authored-by: bertt <538812+bertt@users.noreply.github.com> --- .../outlines/OutlineDetectionTests.cs | 87 +++++++++++++++++++ src/wkb2gltf.core/Triangle.cs | 10 +++ src/wkb2gltf.core/outlines/Adjacency.cs | 14 ++- src/wkb2gltf.core/outlines/Part.cs | 2 +- 4 files changed, 108 insertions(+), 5 deletions(-) diff --git a/src/wkb2gltf.core.tests/outlines/OutlineDetectionTests.cs b/src/wkb2gltf.core.tests/outlines/OutlineDetectionTests.cs index c4220cf5..f735cb4d 100644 --- a/src/wkb2gltf.core.tests/outlines/OutlineDetectionTests.cs +++ b/src/wkb2gltf.core.tests/outlines/OutlineDetectionTests.cs @@ -197,4 +197,91 @@ 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) + + Console.WriteLine($"Adjacency keys: {string.Join(", ", adjacency.Keys)}"); + foreach (var kvp in adjacency) + { + Console.WriteLine($" Triangle {kvp.Key}: {string.Join(", ", kvp.Value)}"); + } + + 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); + + Console.WriteLine($"Outline count: {outlines.Count}"); + + // 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 + // Wait - but if t0 and t1 share an edge and they're not coplanar, that edge would appear in the outline twice (once from each triangle). + // Actually no - looking at the Part.GetOutlines logic, it excludes edges that are in the adjacency list. + // So for t0: edges 0-1, 1-2, 2-0. If edge 1-2 is shared with t2 (coplanar), it's excluded. If edge 0-1 is shared with t1 (not coplanar), it's NOT excluded, so it appears in outline. + // Similarly for t1: if its edge is shared with t0, it's also not in adjacency, so it appears. + // So we get the edge twice? That's correct for outlines - each side of a crease/fold appears as a separate line. + // + // Let me recalculate: + // t0: 3 edges minus 1 (shared with t2) = 2 edges + // t1: 3 edges minus 0 (no coplanar neighbors) = 3 edges + // t2: 3 edges minus 1 (shared with t0) = 2 edges + // Total: 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..f7b23f0a 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 product close to 1.0) or opposite directions (dot product close to -1.0) + return Math.Abs(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/Part.cs b/src/wkb2gltf.core/outlines/Part.cs index 952cd78f..605f0636 100644 --- a/src/wkb2gltf.core/outlines/Part.cs +++ b/src/wkb2gltf.core/outlines/Part.cs @@ -11,7 +11,7 @@ public static List GetOutlines(List triangles, List indice result = GetOutlines(triangles[0], offset: offset + indices[0] * 3); } else if (triangles.Count > 1) { - var adjacency = Adjacency.GetAdjacencyList(triangles, distanceTolerance); + var adjacency = Adjacency.GetAdjacencyList(triangles, distanceTolerance, normalTolerance); var i = 0; foreach (var triangle in triangles) { List<(int from, int to)> val; From dd4586a6bf46783428be6cefefdab78ba0055238 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:18:57 +0000 Subject: [PATCH 5/6] Complete fix for #200: handle coplanar triangles across partitions Co-authored-by: bertt <538812+bertt@users.noreply.github.com> --- .../outlines/OutlineDetectionTests.cs | 19 ------------------ .../outlines/OutlineDetection.cs | 20 +++++++++++++++++-- src/wkb2gltf.core/outlines/Part.cs | 14 ++++++++++--- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/wkb2gltf.core.tests/outlines/OutlineDetectionTests.cs b/src/wkb2gltf.core.tests/outlines/OutlineDetectionTests.cs index f735cb4d..11d1b3e7 100644 --- a/src/wkb2gltf.core.tests/outlines/OutlineDetectionTests.cs +++ b/src/wkb2gltf.core.tests/outlines/OutlineDetectionTests.cs @@ -243,12 +243,6 @@ public void Issue200_CoplanarTrianglesNonSequential() // 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) - Console.WriteLine($"Adjacency keys: {string.Join(", ", adjacency.Keys)}"); - foreach (var kvp in adjacency) - { - Console.WriteLine($" Triangle {kvp.Key}: {string.Join(", ", kvp.Value)}"); - } - Assert.That(adjacency.ContainsKey(0), Is.True, "t0 should have adjacency entries"); Assert.That(adjacency.ContainsKey(2), Is.True, "t2 should have adjacency entries"); @@ -263,24 +257,11 @@ public void Issue200_CoplanarTrianglesNonSequential() var outlines = OutlineDetection.GetOutlines2(triangles); - Console.WriteLine($"Outline count: {outlines.Count}"); - // 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 - // Wait - but if t0 and t1 share an edge and they're not coplanar, that edge would appear in the outline twice (once from each triangle). - // Actually no - looking at the Part.GetOutlines logic, it excludes edges that are in the adjacency list. - // So for t0: edges 0-1, 1-2, 2-0. If edge 1-2 is shared with t2 (coplanar), it's excluded. If edge 0-1 is shared with t1 (not coplanar), it's NOT excluded, so it appears in outline. - // Similarly for t1: if its edge is shared with t0, it's also not in adjacency, so it appears. - // So we get the edge twice? That's correct for outlines - each side of a crease/fold appears as a separate line. - // - // Let me recalculate: - // t0: 3 edges minus 1 (shared with t2) = 2 edges - // t1: 3 edges minus 0 (no coplanar neighbors) = 3 edges - // t2: 3 edges minus 1 (shared with t0) = 2 edges - // Total: 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/outlines/OutlineDetection.cs b/src/wkb2gltf.core/outlines/OutlineDetection.cs index 24f87b79..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, normalTolerance); + + // 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 605f0636..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, double normalTolerance = 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, normalTolerance); + // 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; From 4476330b0446f42f9d2e5edfceab7c0cf860d30c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:21:44 +0000 Subject: [PATCH 6/6] Improve coplanarity check clarity per code review Co-authored-by: bertt <538812+bertt@users.noreply.github.com> --- src/wkb2gltf.core/Triangle.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wkb2gltf.core/Triangle.cs b/src/wkb2gltf.core/Triangle.cs index f7b23f0a..7b0bb408 100644 --- a/src/wkb2gltf.core/Triangle.cs +++ b/src/wkb2gltf.core/Triangle.cs @@ -74,9 +74,9 @@ 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 product close to 1.0) or opposite directions (dot product close to -1.0) - return Math.Abs(Math.Abs(dotProduct) - 1.0f) <= normalTolerance; + // 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