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 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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