diff --git a/benchmark/CDT.Comparison.Benchmarks/CDT.Comparison.Benchmarks.csproj b/benchmark/CDT.Comparison.Benchmarks/CDT.Comparison.Benchmarks.csproj
index ad2b546..6436eb3 100644
--- a/benchmark/CDT.Comparison.Benchmarks/CDT.Comparison.Benchmarks.csproj
+++ b/benchmark/CDT.Comparison.Benchmarks/CDT.Comparison.Benchmarks.csproj
@@ -13,7 +13,7 @@
(e.g., boost_math .NET examples) get picked up as EmbeddedResource
items and cause MSB3822 build errors. -->
- $(DefaultItemExcludes);native\**
+ $(DefaultItemExcludes);native\**\*.res;native\**\build\**;native\**\target\**
diff --git a/benchmark/CDT.Comparison.Benchmarks/ComparisonBenchmarks.cs b/benchmark/CDT.Comparison.Benchmarks/ComparisonBenchmarks.cs
index 64b91a6..c1fa538 100644
--- a/benchmark/CDT.Comparison.Benchmarks/ComparisonBenchmarks.cs
+++ b/benchmark/CDT.Comparison.Benchmarks/ComparisonBenchmarks.cs
@@ -2,6 +2,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using CDT;
@@ -16,7 +17,6 @@
using TnPolygon = TriangleNet.Geometry.Polygon;
using TnSegment = TriangleNet.Geometry.Segment;
using TnVertex = TriangleNet.Geometry.Vertex;
-using System.Runtime.InteropServices;
// ---------------------------------------------------------------------------
// Shared input reader
@@ -69,7 +69,7 @@ public static int VerticesOnly(double[] xs, double[] ys)
for (int i = 0; i < xs.Length; i++)
verts.Add(new V2d(xs[i], ys[i]));
- var cdt = new Triangulation(VertexInsertionOrder.Auto);
+ var cdt = new Triangulation(VertexInsertionOrder.Auto, IntersectingConstraintEdges.TryResolve, 0.0);
cdt.InsertVertices(verts);
return cdt.Triangles.Length;
}
@@ -84,11 +84,27 @@ public static int Constrained(double[] xs, double[] ys, int[] ev1, int[] ev2)
for (int i = 0; i < ev1.Length; i++)
edges.Add(new CdtEdge(ev1[i], ev2[i]));
- var cdt = new Triangulation(VertexInsertionOrder.Auto);
+ var cdt = new Triangulation(VertexInsertionOrder.Auto, IntersectingConstraintEdges.TryResolve, 0.0);
cdt.InsertVertices(verts);
cdt.InsertEdges(edges);
return cdt.Triangles.Length;
}
+
+ public static int Conforming(double[] xs, double[] ys, int[] ev1, int[] ev2)
+ {
+ var verts = new List>(xs.Length);
+ for (int i = 0; i < xs.Length; i++)
+ verts.Add(new V2d(xs[i], ys[i]));
+
+ var edges = new List(ev1.Length);
+ for (int i = 0; i < ev1.Length; i++)
+ edges.Add(new CdtEdge(ev1[i], ev2[i]));
+
+ var cdt = new Triangulation(VertexInsertionOrder.Auto, IntersectingConstraintEdges.TryResolve, 0.0);
+ cdt.InsertVertices(verts);
+ cdt.ConformToEdges(edges);
+ return cdt.Triangles.Length;
+ }
}
// ---------------------------------------------------------------------------
@@ -118,7 +134,32 @@ public static int Constrained(double[] xs, double[] ys, int[] ev1, int[] ev2)
for (int i = 0; i < ev1.Length; i++)
polygon.Add(new TnSegment(verts[ev1[i]], verts[ev2[i]]));
- return new TnMesher().Triangulate(polygon).Triangles.Count;
+ return new TnMesher().Triangulate(polygon, new TriangleNet.Meshing.ConstraintOptions()
+ {
+ ConformingDelaunay = false,
+ SegmentSplitting = 2,
+ Convex = true
+ }).Triangles.Count;
+ }
+
+ public static int Conforming(double[] xs, double[] ys, int[] ev1, int[] ev2)
+ {
+ var polygon = new TnPolygon(xs.Length);
+ var verts = new TnVertex[xs.Length];
+ for (int i = 0; i < xs.Length; i++)
+ {
+ verts[i] = new TnVertex(xs[i], ys[i]);
+ polygon.Add(verts[i]);
+ }
+ for (int i = 0; i < ev1.Length; i++)
+ polygon.Add(new TnSegment(verts[ev1[i]], verts[ev2[i]]));
+
+ return new TnMesher().Triangulate(polygon, new TriangleNet.Meshing.ConstraintOptions()
+ {
+ ConformingDelaunay = true,
+ SegmentSplitting = 2,
+ Convex = true
+ }).Triangles.Count;
}
}
@@ -139,6 +180,7 @@ public static int VerticesOnly(double[] xs, double[] ys)
coords[i] = new NtsCoordinate(xs[i], ys[i]);
var builder = new NetTopologySuite.Triangulate.DelaunayTriangulationBuilder();
+ builder.Tolerance = 0.0;
builder.SetSites(coords);
return builder.GetTriangles(Gf).NumGeometries;
}
@@ -158,6 +200,7 @@ public static int Conforming(double[] xs, double[] ys, int[] ev1, int[] ev2)
});
var builder = new NetTopologySuite.Triangulate.ConformingDelaunayTriangulationBuilder();
+ builder.Tolerance = 0.0;
builder.SetSites(new NtsMultiPoint(pts));
builder.Constraints = new NtsMultiLineString(segments);
return builder.GetTriangles(Gf).NumGeometries;
@@ -178,13 +221,20 @@ private static partial int Triangulate(
double[] xs, double[] ys, int nVerts,
int[] ev1, int[] ev2, int nEdges);
+ [LibraryImport(Lib, EntryPoint = "cdt_conform_d")]
+ private static partial int Conform(
+ double[] xs, double[] ys, int nVerts,
+ int[] ev1, int[] ev2, int nEdges);
+
public static int VerticesOnly(double[] xs, double[] ys) =>
Triangulate(xs, ys, xs.Length, [], [], 0);
public static int Constrained(double[] xs, double[] ys, int[] ev1, int[] ev2) =>
Triangulate(xs, ys, xs.Length, ev1, ev2, ev1.Length);
-}
+ public static int Conforming(double[] xs, double[] ys, int[] ev1, int[] ev2) =>
+ Conform(xs, ys, xs.Length, ev1, ev2, ev1.Length);
+}
// ---------------------------------------------------------------------------
// Adapter — Spade (Rust via P/Invoke, spade 2.15.0)
@@ -201,13 +251,20 @@ private static partial int SpadeTriangulate(
double[] xs, double[] ys, int nVerts,
int[] ev1, int[] ev2, int nEdges);
+ [LibraryImport(Lib, EntryPoint = "spade_conform")]
+ private static partial int SpadeConform(
+ double[] xs, double[] ys, int nVerts,
+ int[] ev1, int[] ev2, int nEdges);
+
public static int VerticesOnly(double[] xs, double[] ys) =>
SpadeTriangulate(xs, ys, xs.Length, [], [], 0);
public static int Constrained(double[] xs, double[] ys, int[] ev1, int[] ev2) =>
SpadeTriangulate(xs, ys, xs.Length, ev1, ev2, ev1.Length);
-}
+ public static int Conforming(double[] xs, double[] ys, int[] ev1, int[] ev2) =>
+ SpadeConform(xs, ys, xs.Length, ev1, ev2, ev1.Length);
+}
// ---------------------------------------------------------------------------
// Adapter — CGAL (C++ via P/Invoke, CGAL 5.x/6.x)
@@ -227,13 +284,20 @@ private static partial int CgalTriangulate(
double[] xs, double[] ys, int nVerts,
int[] ev1, int[] ev2, int nEdges);
+ [LibraryImport(Lib, EntryPoint = "cgal_conform")]
+ private static partial int CgalConform(
+ double[] xs, double[] ys, int nVerts,
+ int[] ev1, int[] ev2, int nEdges);
+
public static int VerticesOnly(double[] xs, double[] ys) =>
CgalTriangulate(xs, ys, xs.Length, [], [], 0);
public static int Constrained(double[] xs, double[] ys, int[] ev1, int[] ev2) =>
CgalTriangulate(xs, ys, xs.Length, ev1, ev2, ev1.Length);
-}
+ public static int Conforming(double[] xs, double[] ys, int[] ev1, int[] ev2) =>
+ CgalConform(xs, ys, xs.Length, ev1, ev2, ev1.Length);
+}
// (~2 600 vertices, ~2 600 constraint edges)
// ---------------------------------------------------------------------------
@@ -258,25 +322,25 @@ public void Setup() =>
[BenchmarkCategory("VerticesOnly")]
public int VO_CdtNet() => CdtNetAdapter.VerticesOnly(_xs, _ys);
- [Benchmark(Description = "Triangle.NET")]
- [BenchmarkCategory("VerticesOnly")]
- public int VO_TriangleNet() => TriangleNetAdapter.VerticesOnly(_xs, _ys);
-
- [Benchmark(Description = "NTS")]
- [BenchmarkCategory("VerticesOnly")]
- public int VO_Nts() => NtsAdapter.VerticesOnly(_xs, _ys);
-
[Benchmark(Description = "artem-ogre/CDT (C++)")]
[BenchmarkCategory("VerticesOnly")]
public int VO_NativeCdt() => NativeCdtAdapter.VerticesOnly(_xs, _ys);
+ [Benchmark(Description = "Spade (Rust)")]
+ [BenchmarkCategory("VerticesOnly")]
+ public int VO_Spade() => SpadeAdapter.VerticesOnly(_xs, _ys);
+
[Benchmark(Description = "CGAL (C++)")]
[BenchmarkCategory("VerticesOnly")]
public int VO_Cgal() => CgalAdapter.VerticesOnly(_xs, _ys);
- [Benchmark(Description = "Spade (Rust)")]
+ [Benchmark(Description = "NTS")]
[BenchmarkCategory("VerticesOnly")]
- public int VO_Spade() => SpadeAdapter.VerticesOnly(_xs, _ys);
+ public int VO_Nts() => NtsAdapter.VerticesOnly(_xs, _ys);
+
+ [Benchmark(Description = "Triangle.NET")]
+ [BenchmarkCategory("VerticesOnly")]
+ public int VO_TriangleNet() => TriangleNetAdapter.VerticesOnly(_xs, _ys);
// -- Constrained ---------------------------------------------------------
@@ -284,23 +348,45 @@ public void Setup() =>
[BenchmarkCategory("Constrained")]
public int CDT_CdtNet() => CdtNetAdapter.Constrained(_xs, _ys, _ev1, _ev2);
- [Benchmark(Description = "Triangle.NET")]
- [BenchmarkCategory("Constrained")]
- public int CDT_TriangleNet() => TriangleNetAdapter.Constrained(_xs, _ys, _ev1, _ev2);
-
- [Benchmark(Description = "NTS (Conforming CDT)")]
- [BenchmarkCategory("Constrained")]
- public int CDT_Nts() => NtsAdapter.Conforming(_xs, _ys, _ev1, _ev2);
-
[Benchmark(Description = "artem-ogre/CDT (C++)")]
[BenchmarkCategory("Constrained")]
public int CDT_NativeCdt() => NativeCdtAdapter.Constrained(_xs, _ys, _ev1, _ev2);
+ [Benchmark(Description = "Spade (Rust)")]
+ [BenchmarkCategory("Constrained")]
+ public int CDT_Spade() => SpadeAdapter.Constrained(_xs, _ys, _ev1, _ev2);
+
[Benchmark(Description = "CGAL (C++)")]
[BenchmarkCategory("Constrained")]
public int CDT_Cgal() => CgalAdapter.Constrained(_xs, _ys, _ev1, _ev2);
- [Benchmark(Description = "Spade (Rust)")]
+ [Benchmark(Description = "Triangle.NET")]
[BenchmarkCategory("Constrained")]
- public int CDT_Spade() => SpadeAdapter.Constrained(_xs, _ys, _ev1, _ev2);
+ public int CDT_TriangleNet() => TriangleNetAdapter.Constrained(_xs, _ys, _ev1, _ev2);
+
+ // - Conforming ----------------------------------------------------------
+
+ [Benchmark(Baseline = true, Description = "CDT.NET")]
+ [BenchmarkCategory("Conforming")]
+ public int CfDT_CdtNet() => CdtNetAdapter.Conforming(_xs, _ys, _ev1, _ev2);
+
+ [Benchmark(Description = "artem-ogre/CDT (C++)")]
+ [BenchmarkCategory("Conforming")]
+ public int CfDT_NativeCdt() => NativeCdtAdapter.Conforming(_xs, _ys, _ev1, _ev2);
+
+ [Benchmark(Description = "Spade (Rust)")]
+ [BenchmarkCategory("Conforming")]
+ public int CfDT_Spade() => SpadeAdapter.Conforming(_xs, _ys, _ev1, _ev2);
+
+ [Benchmark(Description = "CGAL (C++)")]
+ [BenchmarkCategory("Conforming")]
+ public int CfDT_Cgal() => CgalAdapter.Conforming(_xs, _ys, _ev1, _ev2);
+
+ [Benchmark(Description = "NTS")]
+ [BenchmarkCategory("Conforming")]
+ public int CfDT_Nts() => NtsAdapter.Conforming(_xs, _ys, _ev1, _ev2);
+
+ [Benchmark(Description = "Triangle.NET")]
+ [BenchmarkCategory("Conforming")]
+ public int CfDT_TriangleNet() => TriangleNetAdapter.Conforming(_xs, _ys, _ev1, _ev2);
}
diff --git a/benchmark/CDT.Comparison.Benchmarks/Program.cs b/benchmark/CDT.Comparison.Benchmarks/Program.cs
index c9e5352..a3d628f 100644
--- a/benchmark/CDT.Comparison.Benchmarks/Program.cs
+++ b/benchmark/CDT.Comparison.Benchmarks/Program.cs
@@ -4,4 +4,30 @@
using BenchmarkDotNet.Running;
+var bench = new ComparisonBenchmarks();
+bench.Setup();
+
+static void Print(string label, Func compute) =>
+Console.WriteLine($" {label,-22} {compute(),6:N0} triangles");
+
+const int lineWidth = 38;
+Console.WriteLine("Constrained Delaunay Triangulation");
+Console.WriteLine(new string('-', lineWidth));
+Print("CDT.NET", bench.CDT_CdtNet);
+Print("artem-ogre/CDT (C++)", bench.CDT_NativeCdt);
+Print("Spade (Rust)", bench.CDT_Spade);
+Print("CGAL (C++)", bench.CDT_Cgal);
+Print("Triangle.NET", bench.CDT_TriangleNet);
+Console.WriteLine(new string('-', lineWidth));
+
+Console.WriteLine("Conforming Delaunay Triangulation");
+Console.WriteLine(new string('-', lineWidth));
+Print("CDT.NET", bench.CfDT_CdtNet);
+Print("artem-ogre/CDT (C++)", bench.CfDT_NativeCdt);
+Print("Spade (Rust)", bench.CfDT_Spade);
+Print("CGAL (C++)", bench.CfDT_Cgal);
+Print("NTS", bench.CfDT_Nts);
+Print("Triangle.NET", bench.CfDT_TriangleNet);
+Console.WriteLine(new string('-', lineWidth));
+
BenchmarkSwitcher.FromAssembly(typeof(ComparisonBenchmarks).Assembly).Run(args);
diff --git a/benchmark/CDT.Comparison.Benchmarks/README.md b/benchmark/CDT.Comparison.Benchmarks/README.md
index 70b5c1d..c474041 100644
--- a/benchmark/CDT.Comparison.Benchmarks/README.md
+++ b/benchmark/CDT.Comparison.Benchmarks/README.md
@@ -88,30 +88,57 @@ Results are written to `BenchmarkDotNet.Artifacts/` in the current directory.
| CGAL (C++) | `number_of_faces()` counts all finite triangles in the triangulation, consistent with artem-ogre/CDT. First build downloads CGAL 6.1.1 library headers (~10 MB) and the required Boost sub-library headers (~10 MB ZIPs, ~60 MB staged); subsequent builds use the cmake cache. |
| Spade (Rust) | `num_inner_faces()` returns only inner (non-convex-hull) triangles, which is fewer than the C++ CDT counts |
+## Counts of triangles by library and category
+
+```
+Constrained Delaunay Triangulation
+--------------------------------------
+ CDT.NET 5.239 triangles
+ artem-ogre/CDT (C++) 5.239 triangles
+ Spade (Rust) 5.204 triangles
+ CGAL (C++) 5.204 triangles
+ Triangle.NET 5.204 triangles
+--------------------------------------
+Conforming Delaunay Triangulation
+--------------------------------------
+ CDT.NET 5.343 triangles
+ artem-ogre/CDT (C++) 5.343 triangles
+ Spade (Rust) 5.308 triangles
+ CGAL (C++) 5.324 triangles
+ NTS 5.764 triangles
+ Triangle.NET 5.204 triangles
+--------------------------------------
+```
+
## Benchmark results
> 12th Gen Intel Core i7-12700KF 3.60GHz, 1 CPU, 20 logical and 12 physical cores
-| Method | Categories | Mean | Error | StdDev | Ratio | RatioSD |
-|----------------------- |------------- |----------:|----------:|----------:|------:|--------:|
-| **CDT.NET** | Constrained | 1.198 ms | 0.1065 ms | 0.0058 ms | 1.00 | 0.01 |
-| Triangle.NET | Constrained | 4.571 ms | 2.2827 ms | 0.1251 ms | 3.82 | 0.09 |
-| 'NTS (Conforming CDT)' | Constrained | 37.066 ms | 8.5571 ms | 0.4690 ms | 30.95 | 0.36 |
-| 'artem-ogre/CDT (C++)' | Constrained | 1.788 ms | 0.1284 ms | 0.0070 ms | 1.49 | 0.01 |
-| 'CGAL (C++)' | Constrained | 2.538 ms | 0.0574 ms | 0.0031 ms | 2.12 | 0.01 |
-| 'Spade (Rust)' | Constrained | 1.255 ms | 0.1050 ms | 0.0058 ms | 1.05 | 0.01 |
-| | | | | | | |
-| **CDT.NET** | VerticesOnly | 1.048 ms | 0.0371 ms | 0.0020 ms | 1.00 | 0.00 |
-| Triangle.NET | VerticesOnly | 1.323 ms | 0.1856 ms | 0.0102 ms | 1.26 | 0.01 |
-| NTS | VerticesOnly | 5.519 ms | 2.8885 ms | 0.1583 ms | 5.26 | 0.13 |
-| 'CGAL (C++)' | VerticesOnly | 2.063 ms | 0.2154 ms | 0.0118 ms | 1.97 | 0.01 |
-| 'artem-ogre/CDT (C++)' | VerticesOnly | 1.557 ms | 0.1013 ms | 0.0056 ms | 1.49 | 0.01 |
-| 'Spade (Rust)' | VerticesOnly | 1.028 ms | 0.0803 ms | 0.0044 ms | 0.98 | 0.00 |
-
+| Method | Categories | Mean | Error | StdDev | Ratio |
+|----------------------- |------------- |----------:|-----------:|----------:|------:|
+| CDT.NET | Conforming | 1.442 ms | 0.1628 ms | 0.0089 ms | 1.00 |
+| 'artem-ogre/CDT (C++)' | Conforming | 1.976 ms | 0.0501 ms | 0.0027 ms | 1.37 |
+| 'Spade (Rust)' | Conforming | 1.341 ms | 0.2933 ms | 0.0161 ms | 0.93 |
+| 'CGAL (C++)' | Conforming | 4.110 ms | 0.3934 ms | 0.0216 ms | 2.85 |
+| NTS | Conforming | 38.288 ms | 39.8335 ms | 2.1834 ms | 26.55 |
+| Triangle.NET | Conforming | 3.284 ms | 0.6901 ms | 0.0378 ms | 2.28 |
+| | | | | | |
+| CDT.NET | Constrained | 1.167 ms | 0.0737 ms | 0.0040 ms | 1.00 |
+| 'artem-ogre/CDT (C++)' | Constrained | 1.766 ms | 0.0619 ms | 0.0034 ms | 1.51 |
+| 'Spade (Rust)' | Constrained | 1.256 ms | 0.1233 ms | 0.0068 ms | 1.08 |
+| 'CGAL (C++)' | Constrained | 2.613 ms | 0.3773 ms | 0.0207 ms | 2.24 |
+| Triangle.NET | Constrained | 3.290 ms | 1.3341 ms | 0.0731 ms | 2.82 |
+| | | | | | |
+| CDT.NET | VerticesOnly | 1.072 ms | 0.0045 ms | 0.0002 ms | 1.00 |
+| 'artem-ogre/CDT (C++)' | VerticesOnly | 1.568 ms | 0.2550 ms | 0.0140 ms | 1.46 |
+| 'Spade (Rust)' | VerticesOnly | 1.038 ms | 0.0224 ms | 0.0012 ms | 0.97 |
+| 'CGAL (C++)' | VerticesOnly | 2.156 ms | 0.2064 ms | 0.0113 ms | 2.01 |
+| NTS | VerticesOnly | 5.608 ms | 2.5000 ms | 0.1370 ms | 5.23 |
+| Triangle.NET | VerticesOnly | 1.355 ms | 0.0418 ms | 0.0023 ms | 1.26 |
### Key takeaways
-- **CDT.NET matches the original C++ implementation (artem-ogre/CDT) and Spade within ≤13%** on both constrained and unconstrained triangulation.
+- **CDT.NET matches the original C++ implementation (artem-ogre/CDT) and Spade within ≤13%**.
- **CGAL** runs at ~2× CDT.NET. CGAL's `Constrained_Delaunay_triangulation_2` uses a more complex data structure (half-edge DCEL) with additional bookkeeping overhead vs. CDT.NET's compact flat arrays. For raw triangulation throughput CDT.NET is faster.
- **CDT.NET allocates 5–120× less managed memory** than Triangle.NET and NTS: Triangle.NET allocates ~5.7× more, NTS ~121× more.
- **NTS (conforming CDT)** is ~30× slower and allocates ~120× more memory — Steiner-point insertion is the main cost, and the result is semantically different (not true CDT).
diff --git a/benchmark/CDT.Comparison.Benchmarks/native/cdt_wrapper/cdt_wrapper.cpp b/benchmark/CDT.Comparison.Benchmarks/native/cdt_wrapper/cdt_wrapper.cpp
index 81444c9..ffe4674 100644
--- a/benchmark/CDT.Comparison.Benchmarks/native/cdt_wrapper/cdt_wrapper.cpp
+++ b/benchmark/CDT.Comparison.Benchmarks/native/cdt_wrapper/cdt_wrapper.cpp
@@ -51,4 +51,49 @@ int32_t cdt_triangulate_d(
}
}
+/// Triangulate points with constraint edges using artem-ogre/CDT conforming mode.
+/// Uses conformToEdges() which inserts Steiner points (midpoints) along each
+/// constraint edge until it is represented by a chain of triangulation edges,
+/// producing a Conforming Delaunay Triangulation.
+/// Returns the number of triangles, or -1 on error.
+int32_t cdt_conform_d(
+ const double* xs,
+ const double* ys,
+ int32_t n_verts,
+ const int32_t* edge_v1,
+ const int32_t* edge_v2,
+ int32_t n_edges)
+{
+ try
+ {
+ CDT::Triangulation cdt(
+ CDT::VertexInsertionOrder::Auto,
+ CDT::IntersectingConstraintEdges::TryResolve,
+ 0.0);
+
+ std::vector> verts;
+ verts.reserve(static_cast(n_verts));
+ for (int32_t i = 0; i < n_verts; ++i)
+ verts.push_back(CDT::V2d(xs[i], ys[i]));
+ cdt.insertVertices(verts);
+
+ if (n_edges > 0)
+ {
+ std::vector edges;
+ edges.reserve(static_cast(n_edges));
+ for (int32_t i = 0; i < n_edges; ++i)
+ edges.emplace_back(
+ static_cast(edge_v1[i]),
+ static_cast(edge_v2[i]));
+ cdt.conformToEdges(edges);
+ }
+
+ return static_cast(cdt.triangles.size());
+ }
+ catch (...)
+ {
+ return -1;
+ }
+}
+
} // extern "C"
diff --git a/benchmark/CDT.Comparison.Benchmarks/native/cgal_wrapper/CMakeLists.txt b/benchmark/CDT.Comparison.Benchmarks/native/cgal_wrapper/CMakeLists.txt
index d1d351c..ee1c2bf 100644
--- a/benchmark/CDT.Comparison.Benchmarks/native/cgal_wrapper/CMakeLists.txt
+++ b/benchmark/CDT.Comparison.Benchmarks/native/cgal_wrapper/CMakeLists.txt
@@ -44,11 +44,11 @@ set(_BOOST_TAG "boost-1.87.0")
# Complete list of Boost sub-libraries required by CGAL 6.1.1 CDT (Epick):
set(_BOOST_LIBS
algorithm any array assert concept_check config container container_hash
- core describe detail exception foreach functional fusion integer
- intrusive io iterator lexical_cast math move mp11 mpl multiprecision
+ core describe detail exception foreach functional fusion graph integer
+ intrusive io iterator lexical_cast math move mp11 mpl multiprecision multi_index
numeric_conversion optional predef preprocessor property_map random
- range smart_ptr static_assert throw_exception tuple type_index
- type_traits utility
+ range smart_ptr static_assert stl_interfaces throw_exception tuple typeof type_index
+ type_traits unordered utility
)
set(_BOOST_STAGING "${CMAKE_BINARY_DIR}/boost_headers")
diff --git a/benchmark/CDT.Comparison.Benchmarks/native/cgal_wrapper/cgal_wrapper.cpp b/benchmark/CDT.Comparison.Benchmarks/native/cgal_wrapper/cgal_wrapper.cpp
index 098934f..5b878a5 100644
--- a/benchmark/CDT.Comparison.Benchmarks/native/cgal_wrapper/cgal_wrapper.cpp
+++ b/benchmark/CDT.Comparison.Benchmarks/native/cgal_wrapper/cgal_wrapper.cpp
@@ -9,6 +9,8 @@
#include
#include
+#include
+#include
#include
#include
@@ -17,6 +19,12 @@ typedef CGAL::Constrained_Delaunay_triangulation_2 CDT;
typedef CDT::Point Point;
typedef CDT::Vertex_handle Vertex_handle;
+// For conforming CDT: wrap CDT with Exact_predicates_tag so the conformer
+// can split constraint edges, then apply make_conforming_Delaunay_2.
+typedef CGAL::Constrained_Delaunay_triangulation_2<
+ K, CGAL::Default, CGAL::Exact_predicates_tag> CDT_Conform_Base;
+typedef CGAL::Constrained_triangulation_plus_2 CDTP;
+
extern "C" {
/// Triangulate points with optional constraint edges using CGAL.
@@ -50,4 +58,38 @@ int32_t cgal_cdt(
}
}
+/// Triangulate points with constraint edges using CGAL Conforming Delaunay.
+/// Uses Constrained_triangulation_plus_2 + make_conforming_Delaunay_2, which
+/// inserts Steiner points until every constrained edge is a Delaunay edge.
+/// Returns the total number of faces (triangles), or -1 on error.
+int32_t cgal_conform(
+ const double* xs,
+ const double* ys,
+ int32_t n_verts,
+ const int32_t* edge_v1,
+ const int32_t* edge_v2,
+ int32_t n_edges)
+{
+ try
+ {
+ CDTP cdt;
+
+ std::vector handles;
+ handles.reserve(static_cast(n_verts));
+ for (int32_t i = 0; i < n_verts; ++i)
+ handles.push_back(cdt.insert(Point(xs[i], ys[i])));
+
+ for (int32_t i = 0; i < n_edges; ++i)
+ cdt.insert_constraint(handles[edge_v1[i]], handles[edge_v2[i]]);
+
+ CGAL::make_conforming_Delaunay_2(cdt);
+
+ return static_cast(cdt.number_of_faces());
+ }
+ catch (...)
+ {
+ return -1;
+ }
+}
+
} // extern "C"
diff --git a/benchmark/CDT.Comparison.Benchmarks/native/spade_wrapper/src/lib.rs b/benchmark/CDT.Comparison.Benchmarks/native/spade_wrapper/src/lib.rs
index df58e0b..2ba22eb 100644
--- a/benchmark/CDT.Comparison.Benchmarks/native/spade_wrapper/src/lib.rs
+++ b/benchmark/CDT.Comparison.Benchmarks/native/spade_wrapper/src/lib.rs
@@ -4,6 +4,44 @@
use std::panic;
use spade::{ConstrainedDelaunayTriangulation, Point2, Triangulation};
+use spade::handles::FixedVertexHandle;
+
+type Cdt = ConstrainedDelaunayTriangulation>;
+
+/// Returns true if h1 and h2 are already directly connected by a triangulation edge.
+fn is_direct_edge(cdt: &Cdt, h1: FixedVertexHandle, h2: FixedVertexHandle) -> bool {
+ cdt.vertex(h1).out_edges().any(|e| e.to().fix() == h2)
+}
+
+/// Returns the midpoint coordinates of the segment between h1 and h2.
+fn midpoint(cdt: &Cdt, h1: FixedVertexHandle, h2: FixedVertexHandle) -> (f64, f64) {
+ let p1 = cdt.vertex(h1).position();
+ let p2 = cdt.vertex(h2).position();
+ ((p1.x + p2.x) / 2.0, (p1.y + p2.y) / 2.0)
+}
+
+/// Recursively splits the segment (h1, h2) at its midpoint until the two
+/// endpoints are already connected by a triangulation edge, then marks that
+/// edge as a constraint. This mirrors CDT.NET's ConformToEdges behaviour.
+fn conform_edge(cdt: &mut Cdt, h1: FixedVertexHandle, h2: FixedVertexHandle, depth: i32) {
+ if h1 == h2 || depth > 52 {
+ return;
+ }
+ if is_direct_edge(cdt, h1, h2) {
+ let _ = cdt.add_constraint(h1, h2);
+ return;
+ }
+ let (mx, my) = midpoint(cdt, h1, h2);
+ let hm = match cdt.insert(Point2::new(mx, my)) {
+ Ok(h) => h,
+ Err(_) => return,
+ };
+ if hm == h1 || hm == h2 {
+ return;
+ }
+ conform_edge(cdt, h1, hm, depth + 1);
+ conform_edge(cdt, hm, h2, depth + 1);
+}
/// Triangulate points with optional constraint edges using Spade.
/// Returns the number of inner (finite) triangles, or -1 on error/panic.
@@ -54,3 +92,55 @@ pub unsafe extern "C" fn spade_cdt(
result.unwrap_or(-1)
}
+
+/// Conforming Delaunay Triangulation using Spade.
+/// For each constraint edge (v1, v2) the segment is recursively split at its
+/// midpoint until both endpoints are already connected by a triangulation edge,
+/// at which point that edge is marked as a constraint. This mirrors the
+/// behaviour of CDT.NET's ConformToEdges / artem-ogre/CDT's conformToEdges.
+/// Returns the number of inner (finite) triangles, or -1 on error/panic.
+///
+/// # Safety
+/// All pointer arguments must be valid for `n_verts` / `n_edges` reads.
+#[no_mangle]
+pub unsafe extern "C" fn spade_conform(
+ xs: *const f64,
+ ys: *const f64,
+ n_verts: i32,
+ edge_v1: *const i32,
+ edge_v2: *const i32,
+ n_edges: i32,
+) -> i32 {
+ let result = panic::catch_unwind(|| {
+ let n = n_verts as usize;
+ let ne = n_edges as usize;
+
+ let mut cdt = Cdt::new();
+ let mut handles = Vec::with_capacity(n);
+
+ for i in 0..n {
+ let x = *xs.add(i);
+ let y = *ys.add(i);
+ match cdt.insert(Point2::new(x, y)) {
+ Ok(h) => handles.push(h),
+ Err(_) => {
+ if let Some(&last) = handles.last() {
+ handles.push(last);
+ }
+ }
+ }
+ }
+
+ for i in 0..ne {
+ let v1 = *edge_v1.add(i) as usize;
+ let v2 = *edge_v2.add(i) as usize;
+ if v1 < handles.len() && v2 < handles.len() && v1 != v2 {
+ conform_edge(&mut cdt, handles[v1], handles[v2], 0);
+ }
+ }
+
+ cdt.num_inner_faces() as i32
+ });
+
+ result.unwrap_or(-1)
+}