Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions src/wkb2gltf.core.tests/outlines/OutlineDetectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Triangle> { 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");
}

}
10 changes: 10 additions & 0 deletions src/wkb2gltf.core/Triangle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,14 @@ public List<Point> 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;
}

}
14 changes: 10 additions & 4 deletions src/wkb2gltf.core/outlines/Adjacency.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ namespace Wkb2Gltf.outlines;
public static class Adjacency
{
/// <summary>
///
/// 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.
/// </summary>
public static Dictionary<int, List<(int from, int to)>> GetAdjacencyList(List<Triangle> triangles, double distanceTolerance = 0.01)
public static Dictionary<int, List<(int from, int to)>> GetAdjacencyList(List<Triangle> triangles, double distanceTolerance = 0.01, double normalTolerance = 0.01)
{
var res = new Dictionary<int, List<(int from, int to)>>();

Expand All @@ -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]);
}
}
}
}
Expand Down
20 changes: 18 additions & 2 deletions src/wkb2gltf.core/outlines/OutlineDetection.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using SharpGLTF.Geometry;
using SharpGLTF.Schema2;
Expand Down Expand Up @@ -32,11 +33,26 @@ public static List<uint> GetOutlines2(List<Triangle> triangles, double normalTol
{

var outlines = new List<uint>();

// 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<int, List<(int from, int to)>>();
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;
Expand Down
14 changes: 11 additions & 3 deletions src/wkb2gltf.core/outlines/Part.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@
namespace Wkb2Gltf.outlines;
public static class Part
{
public static List<uint> GetOutlines(List<Triangle> triangles, List<uint> indices, uint offset = 0, double distanceTolerance = 0.01)
public static List<uint> GetOutlines(List<Triangle> triangles, List<uint> indices, uint offset = 0, double distanceTolerance = 0.01, double normalTolerance = 0.01, Dictionary<int, List<(int from, int to)>> adjacency = null)
{
var result = new List<uint>();
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;
Expand Down
Loading