From 7cfd75c7a8e3d317d40d3fb6922a4c9160f4a42a Mon Sep 17 00:00:00 2001 From: MichaC Date: Tue, 24 Feb 2026 06:56:24 +0100 Subject: [PATCH 1/4] progres --- .../CDT.Comparison.Benchmarks.csproj | 2 +- .../ComparisonBenchmarks.cs | 72 ++++++++++++++++--- .../CDT.Comparison.Benchmarks/Program.cs | 22 ++++++ 3 files changed, 84 insertions(+), 12 deletions(-) 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..4fdbe24 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; @@ -185,7 +228,6 @@ public static int Constrained(double[] xs, double[] ys, int[] ev1, int[] ev2) => Triangulate(xs, ys, xs.Length, ev1, ev2, ev1.Length); } - // --------------------------------------------------------------------------- // Adapter — Spade (Rust via P/Invoke, spade 2.15.0) // Incremental CDT using Spade's ConstrainedDelaunayTriangulation. @@ -208,7 +250,6 @@ public static int Constrained(double[] xs, double[] ys, int[] ev1, int[] ev2) => SpadeTriangulate(xs, ys, xs.Length, ev1, ev2, ev1.Length); } - // --------------------------------------------------------------------------- // Adapter — CGAL (C++ via P/Invoke, CGAL 5.x/6.x) // Uses the Exact_predicates_inexact_constructions_kernel (Epick): @@ -234,7 +275,6 @@ public static int Constrained(double[] xs, double[] ys, int[] ev1, int[] ev2) => CgalTriangulate(xs, ys, xs.Length, ev1, ev2, ev1.Length); } - // (~2 600 vertices, ~2 600 constraint edges) // --------------------------------------------------------------------------- [MemoryDiagnoser] @@ -288,10 +328,6 @@ public void Setup() => [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); @@ -303,4 +339,18 @@ public void Setup() => [Benchmark(Description = "Spade (Rust)")] [BenchmarkCategory("Constrained")] public int CDT_Spade() => SpadeAdapter.Constrained(_xs, _ys, _ev1, _ev2); + + // - Conforming ---------------------------------------------------------- + + [Benchmark(Description = "CDT.NET")] + [BenchmarkCategory("Conforming")] + public int CfDT_CdtNet() => CdtNetAdapter.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..bbed703 100644 --- a/benchmark/CDT.Comparison.Benchmarks/Program.cs +++ b/benchmark/CDT.Comparison.Benchmarks/Program.cs @@ -4,4 +4,26 @@ using BenchmarkDotNet.Running; +var bench = new ComparisonBenchmarks(); +bench.Setup(); + +static void Print(string label, int triangles) => + Console.WriteLine($" {label,-22} {triangles,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"); +Print("CDT.NET", bench.CfDT_CdtNet()); +Print("NTS", bench.CfDT_Nts()); +Print("Triangle.NET", bench.CfDT_TriangleNet()); +Console.WriteLine(new string('-', lineWidth)); + BenchmarkSwitcher.FromAssembly(typeof(ComparisonBenchmarks).Assembly).Run(args); From d758255319ef8daba23c146468c9a123cb7b93fd Mon Sep 17 00:00:00 2001 From: MichaC Date: Tue, 24 Feb 2026 08:05:22 +0100 Subject: [PATCH 2/4] works --- .../ComparisonBenchmarks.cs | 68 ++++++++++---- .../CDT.Comparison.Benchmarks/Program.cs | 24 ++--- .../native/cdt_wrapper/cdt_wrapper.cpp | 45 ++++++++++ .../native/cgal_wrapper/CMakeLists.txt | 8 +- .../native/cgal_wrapper/cgal_wrapper.cpp | 42 +++++++++ .../native/spade_wrapper/src/lib.rs | 90 +++++++++++++++++++ 6 files changed, 247 insertions(+), 30 deletions(-) diff --git a/benchmark/CDT.Comparison.Benchmarks/ComparisonBenchmarks.cs b/benchmark/CDT.Comparison.Benchmarks/ComparisonBenchmarks.cs index 4fdbe24..47f3c58 100644 --- a/benchmark/CDT.Comparison.Benchmarks/ComparisonBenchmarks.cs +++ b/benchmark/CDT.Comparison.Benchmarks/ComparisonBenchmarks.cs @@ -221,11 +221,19 @@ 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); } // --------------------------------------------------------------------------- @@ -243,11 +251,19 @@ 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); } // --------------------------------------------------------------------------- @@ -268,11 +284,19 @@ 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) @@ -298,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 --------------------------------------------------------- @@ -324,21 +348,21 @@ 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 = "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 ---------------------------------------------------------- @@ -346,6 +370,18 @@ public void Setup() => [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); diff --git a/benchmark/CDT.Comparison.Benchmarks/Program.cs b/benchmark/CDT.Comparison.Benchmarks/Program.cs index bbed703..a3d628f 100644 --- a/benchmark/CDT.Comparison.Benchmarks/Program.cs +++ b/benchmark/CDT.Comparison.Benchmarks/Program.cs @@ -7,23 +7,27 @@ var bench = new ComparisonBenchmarks(); bench.Setup(); -static void Print(string label, int triangles) => - Console.WriteLine($" {label,-22} {triangles,6:N0} triangles"); +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()); +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"); -Print("CDT.NET", bench.CfDT_CdtNet()); -Print("NTS", bench.CfDT_Nts()); -Print("Triangle.NET", bench.CfDT_TriangleNet()); +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/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) +} From 1ebb545478371172d6cac43b25a02ace1b949f60 Mon Sep 17 00:00:00 2001 From: MichaC Date: Tue, 24 Feb 2026 08:59:57 +0100 Subject: [PATCH 3/4] up --- benchmark/CDT.Comparison.Benchmarks/ComparisonBenchmarks.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/CDT.Comparison.Benchmarks/ComparisonBenchmarks.cs b/benchmark/CDT.Comparison.Benchmarks/ComparisonBenchmarks.cs index 47f3c58..c1fa538 100644 --- a/benchmark/CDT.Comparison.Benchmarks/ComparisonBenchmarks.cs +++ b/benchmark/CDT.Comparison.Benchmarks/ComparisonBenchmarks.cs @@ -366,7 +366,7 @@ public void Setup() => // - Conforming ---------------------------------------------------------- - [Benchmark(Description = "CDT.NET")] + [Benchmark(Baseline = true, Description = "CDT.NET")] [BenchmarkCategory("Conforming")] public int CfDT_CdtNet() => CdtNetAdapter.Conforming(_xs, _ys, _ev1, _ev2); From fdbeac4d69811e09ef312ca3dd33d8b128692868 Mon Sep 17 00:00:00 2001 From: MichaC Date: Tue, 24 Feb 2026 09:04:35 +0100 Subject: [PATCH 4/4] final --- benchmark/CDT.Comparison.Benchmarks/README.md | 61 +++++++++++++------ 1 file changed, 44 insertions(+), 17 deletions(-) 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).