diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..2158b31 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,126 @@ +name: CI/CD + +on: + push: + branches: [ main ] + tags: [ 'v*.*.*' ] + pull_request: + branches: [ main ] + workflow_dispatch: + +jobs: + build-and-test: + name: Build and Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-latest] + permissions: + contents: read + pull-requests: write # needed for sticky-pull-request-comment + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Required for SourceLink + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 10.0.x + + # Windows: restore full solution (includes Windows-only CDT.Viz) + - name: Restore dependencies + if: runner.os == 'Windows' + run: dotnet restore + + # Linux: restore only cross-platform projects (CDT.Viz targets Windows only) + - name: Restore dependencies + if: runner.os == 'Linux' + run: | + dotnet restore src/CDT.Core/CDT.Core.csproj + dotnet restore test/CDT.Tests/CDT.Tests.csproj + + # Windows: build full solution + - name: Build + if: runner.os == 'Windows' + run: dotnet build --no-restore -c Release + + # Linux: build only cross-platform projects + - name: Build + if: runner.os == 'Linux' + run: | + dotnet build --no-restore -c Release src/CDT.Core/CDT.Core.csproj + dotnet build --no-restore -c Release test/CDT.Tests/CDT.Tests.csproj + + # Windows: test full solution + - name: Test + if: runner.os == 'Windows' + run: dotnet test --no-build -c Release --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=${{ github.workspace }}/coverage/coverage.cobertura.xml + + # Linux: test only CDT.Tests (CDT.Viz has no tests; benchmark is not a test project) + - name: Test + if: runner.os == 'Linux' + run: dotnet test --no-build -c Release --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=${{ github.workspace }}/coverage/coverage.cobertura.xml test/CDT.Tests/CDT.Tests.csproj + + - name: Generate coverage report + uses: danielpalme/ReportGenerator-GitHub-Action@5 + with: + reports: ${{ github.workspace }}/coverage/coverage.cobertura.xml + targetdir: coveragereport + reporttypes: MarkdownSummaryGithub + + - name: Write coverage to job summary (Windows) + if: runner.os == 'Windows' + run: Get-Content coveragereport/SummaryGithub.md >> $env:GITHUB_STEP_SUMMARY + shell: pwsh + + - name: Add coverage PR comment + uses: marocchino/sticky-pull-request-comment@v2 + if: runner.os == 'Windows' && github.event_name == 'pull_request' + with: + recreate: true + path: coveragereport/SummaryGithub.md + + - name: Write coverage to job summary (Linux) + if: runner.os == 'Linux' + run: cat coveragereport/SummaryGithub.md >> $GITHUB_STEP_SUMMARY + + publish: + name: Publish to NuGet + needs: build-and-test + runs-on: windows-latest + if: startsWith(github.ref, 'refs/tags/v') + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Required for SourceLink + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 10.0.x + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --no-restore -c Release + + - name: Pack + run: dotnet pack src/CDT.Core/CDT.Core.csproj --no-build -c Release -o ./artifacts + + - name: Push to NuGet.org + # Requires a NUGET_API_KEY secret configured in: + # GitHub → Repository Settings → Secrets and variables → Actions → New repository secret + run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/.github/workflows/upstream-sync.yml b/.github/workflows/upstream-sync.yml new file mode 100644 index 0000000..8561612 --- /dev/null +++ b/.github/workflows/upstream-sync.yml @@ -0,0 +1,114 @@ +name: Upstream Sync + +on: + schedule: + # Run daily at 06:00 UTC + - cron: '0 6 * * *' + workflow_dispatch: + +jobs: + check-upstream: + name: Check for upstream changes + runs-on: ubuntu-latest + permissions: + contents: write # needed to update .last-sync-commit on main + issues: write # needed to create the issue that triggers Copilot + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: main + + - name: Get last synced commit + id: last-sync + run: echo "sha=$(cat .last-sync-commit | tr -d '[:space:]')" >> "$GITHUB_OUTPUT" + + - name: Get latest upstream commit + id: upstream + run: | + # git ls-remote reads public repos without any auth or API calls + LATEST=$(git ls-remote https://github.com/artem-ogre/CDT.git refs/heads/master | cut -f1) + if [ -z "$LATEST" ]; then + echo "::error::Could not resolve latest upstream commit SHA" + exit 1 + fi + echo "sha=$LATEST" >> "$GITHUB_OUTPUT" + echo "Latest upstream commit: $LATEST" + + - name: Create Copilot porting issue + if: steps.last-sync.outputs.sha != steps.upstream.outputs.sha + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + LAST="${{ steps.last-sync.outputs.sha }}" + LATEST="${{ steps.upstream.outputs.sha }}" + DATE=$(date -u +"%Y-%m-%d") + + echo "Upstream has new commits since ${LAST:0:7} (latest: ${LATEST:0:7})" + + # Shallow-clone the upstream history (metadata only) to build a commit list + git clone --depth 50 --no-checkout https://github.com/artem-ogre/CDT.git /tmp/CDT-upstream + COMMITS=$(git -C /tmp/CDT-upstream log \ + --pretty=format:"- [%h](https://github.com/artem-ogre/CDT/commit/%H) %s" \ + "${LAST}..${LATEST}" 2>/dev/null || echo "- (see compare link below)") + [ -z "$COMMITS" ] && COMMITS="- (see compare link below)" + + # Update .last-sync-commit on main so we don't re-open the issue next run + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + echo "$LATEST" > .last-sync-commit + git add .last-sync-commit + git commit -m "chore: advance upstream sync marker to ${LATEST:0:7}" + git push origin main + + # Ensure the auto-sync label exists + gh label create "auto-sync" \ + --description "Automatically generated upstream sync issue" \ + --color "0075ca" 2>/dev/null || true + + # Create an issue assigned to the Copilot coding agent. + # Assigning to @Copilot triggers the coding agent to pick it up + # and create a PR with the ported C# changes. + ISSUE_BODY=$(cat <<'EOF' + ## Upstream Sync Task + + @Copilot please port the upstream changes listed below to C# and open a pull request. + + New commits have been pushed to [artem-ogre/CDT](https://github.com/artem-ogre/CDT) since the last sync. + + **Previous synced commit:** [`LAST_SHA`](https://github.com/artem-ogre/CDT/commit/LAST_FULL) + **New upstream commit:** [`LATEST_SHA`](https://github.com/artem-ogre/CDT/commit/LATEST_FULL) + **Full diff:** https://github.com/artem-ogre/CDT/compare/LAST_FULL...LATEST_FULL + + ### Upstream commits to port + + COMMIT_LIST + + ### What to port + + Please port all upstream C++ changes to C# and open a pull request. + + Key files to check in the upstream diff: + - `CDT/include/CDT.h` and `CDT/include/CDT.hpp` → port any algorithm changes to `src/CDT.Core/` + - `CDT/include/Predicates.h` → port to `src/CDT.Core/Predicates.cs` + - `CDT/include/KDTree.h` → port to `src/CDT.Core/KdTree.cs` + - Any new types or utilities → port to `src/CDT.Core/Types.cs` or new files as appropriate + + After porting: + 1. Run `dotnet test` and ensure all tests pass + 2. Update test inputs/expected outputs if needed + EOF + ) + # Replace placeholders with actual values + ISSUE_BODY="${ISSUE_BODY//LAST_SHA/${LAST:0:7}}" + ISSUE_BODY="${ISSUE_BODY//LAST_FULL/${LAST}}" + ISSUE_BODY="${ISSUE_BODY//LATEST_SHA/${LATEST:0:7}}" + ISSUE_BODY="${ISSUE_BODY//LATEST_FULL/${LATEST}}" + ISSUE_BODY="${ISSUE_BODY//COMMIT_LIST/$COMMITS}" + + gh issue create \ + --title "Port upstream CDT changes to C# (${LATEST:0:7}) — ${DATE}" \ + --label "auto-sync" \ + --assignee "Copilot" \ + --body "$ISSUE_BODY" diff --git a/.last-sync-commit b/.last-sync-commit new file mode 100644 index 0000000..1f2034d --- /dev/null +++ b/.last-sync-commit @@ -0,0 +1 @@ +7bd85e41a7b2e6e6e3bf82f36bcbc2bcec6441c5 diff --git a/README.md b/README.md index 6e514ee..988a1c9 100644 --- a/README.md +++ b/README.md @@ -1 +1,217 @@ -"# CDT.NET" +# CDT.NET + +[![Build](https://github.com/MichaCo/CDT.NET/actions/workflows/ci-cd.yml/badge.svg?branch=main)](https://github.com/MichaCo/CDT.NET/actions/workflows/ci-cd.yml) +[![NuGet](https://img.shields.io/nuget/v/CDT.NET.svg)](https://www.nuget.org/packages/CDT.NET) + +A C# port of the [artem-ogre/CDT](https://github.com/artem-ogre/CDT) Constrained Delaunay Triangulation library. + +> **Credits:** This library is a C# port of the excellent [CDT C++ library](https://github.com/artem-ogre/CDT) by Artem Amirkhanov and contributors, licensed under MPL 2.0. +> For full algorithm documentation, research references, and in-depth API documentation please refer to the [original C++ repository](https://github.com/artem-ogre/CDT) and its [online documentation](https://artem-ogre.github.io/CDT/). + +## What is CDT? + +CDT is a library for generating **Constrained** and **Conforming** Delaunay Triangulations. It produces triangulations from a set of points and optional boundary/constraint edges. Unlike a plain Delaunay triangulation, CDT guarantees that the constraint edges you specify will appear in the final mesh. + +## Features + +- **Constrained Delaunay Triangulation** — force specific edges into the triangulation +- **Conforming Delaunay Triangulation** — split edges and add Steiner points until constraint edges are present in the triangulation +- **Convex-hull** triangulation — triangulate all points without any constraints +- **Automatic hole detection** — use `EraseOuterTrianglesAndHoles` to remove outer regions and holes based on even–odd winding depth +- **Robust geometric predicates** — numerically stable orientation and in-circle tests +- **KD-tree spatial indexing** — fast nearest-neighbor lookup during vertex insertion +- **Duplicate handling** — utilities to remove duplicate vertices and remap edges before triangulation +- **Intersecting constraint edges** — optionally resolve by splitting edges at the intersection point +- Multi-target: **.NET 8** and **.NET 10** + +**Pre-conditions** (same as the C++ original): +- No duplicate vertices (use `CdtUtils.RemoveDuplicatesAndRemapEdges` to clean input) +- No two constraint edges may intersect (or pass `IntersectingConstraintEdges.TryResolve`) + +**Post-conditions:** +- All triangles have **counter-clockwise (CCW) winding** in a coordinate system where X points right and Y points up. + +## Installation + +``` +dotnet add package CDT.NET +``` + +## Usage + +### Delaunay triangulation (convex hull, no constraints) + +Insert vertices and call `EraseSuperTriangle` to get the convex-hull triangulation. + +```csharp +using CDT; + +var vertices = new List> +{ + new(0, 0), new(4, 0), new(4, 4), new(0, 4), new(2, 2), +}; + +var cdt = new Triangulation(); +cdt.InsertVertices(vertices); +cdt.EraseSuperTriangle(); // produces convex hull + +IReadOnlyList triangles = cdt.Triangles; +IReadOnlyList> points = cdt.Vertices; +HashSet allEdges = CdtUtils.ExtractEdgesFromTriangles(triangles); +``` + +### Constrained Delaunay triangulation (bounded domain) + +Insert boundary edges, then call `EraseOuterTriangles` to keep only the region inside the boundary. + +```csharp +using CDT; + +var vertices = new List> +{ + new(0, 0), new(4, 0), new(4, 4), new(0, 4), +}; +var edges = new List +{ + new(0, 1), new(1, 2), new(2, 3), new(3, 0), // square boundary +}; + +var cdt = new Triangulation(); +cdt.InsertVertices(vertices); +cdt.InsertEdges(edges); +cdt.EraseOuterTriangles(); // removes everything outside the boundary + +IReadOnlyList triangles = cdt.Triangles; +IReadOnlySet fixedEdges = cdt.FixedEdges; // the constraint edges +``` + +### Auto-detect boundaries and holes + +Use `EraseOuterTrianglesAndHoles` to automatically remove the outer region **and** fill holes. The algorithm uses an even–odd depth rule: depth 0 = outside, depth 1 = inside, depth 2 = hole, etc. + +```csharp +using CDT; + +// Outer square (vertices 0-3) + inner square hole (vertices 4-7) +var vertices = new List> +{ + new(0, 0), new(6, 0), new(6, 6), new(0, 6), // outer square + new(2, 2), new(4, 2), new(4, 4), new(2, 4), // inner hole +}; +var edges = new List +{ + new(0, 1), new(1, 2), new(2, 3), new(3, 0), // outer boundary (CCW) + new(4, 7), new(7, 6), new(6, 5), new(5, 4), // inner hole (CW — opposite winding) +}; + +var cdt = new Triangulation(); +cdt.InsertVertices(vertices); +cdt.InsertEdges(edges); +cdt.EraseOuterTrianglesAndHoles(); + +IReadOnlyList triangles = cdt.Triangles; +``` + +### Conforming Delaunay triangulation + +Use `ConformToEdges` instead of `InsertEdges`. The algorithm may split constraint edges and insert Steiner points (midpoints) until the constraint is represented in the triangulation. + +```csharp +using CDT; + +var vertices = new List> +{ + new(0, 0), new(4, 0), new(4, 4), new(0, 4), +}; +var edges = new List +{ + new(0, 1), new(1, 2), new(2, 3), new(3, 0), +}; + +var cdt = new Triangulation(); +cdt.InsertVertices(vertices); +cdt.ConformToEdges(edges); // may add Steiner points +cdt.EraseOuterTriangles(); +``` + +### Removing duplicate vertices and remapping edges + +Input data often contains duplicate vertices (e.g., from shared polygon boundaries). Use `CdtUtils.RemoveDuplicatesAndRemapEdges` to clean up before triangulation. + +```csharp +using CDT; + +var vertices = new List> +{ + new(0, 0), new(4, 0), new(4, 4), new(0, 4), + new(0, 0), // duplicate of vertex 0 +}; +var edges = new List +{ + new(0, 4), // will be remapped since vertex 4 is a duplicate of vertex 0 + new(1, 2), +}; + +CdtUtils.RemoveDuplicatesAndRemapEdges(vertices, edges); +// vertices now has 4 entries; degenerate self-edges like (0,0) can be dropped + +var cdt = new Triangulation(); +cdt.InsertVertices(vertices); +cdt.InsertEdges(edges.Where(e => e.V1 != e.V2).ToList()); +cdt.EraseSuperTriangle(); +``` + +### Resolving intersecting constraint edges + +By default, intersecting constraint edges throw an exception. Pass `IntersectingConstraintEdges.TryResolve` to split them at their intersection point instead. + +```csharp +using CDT; + +// Two diagonals of a unit square that cross each other +var vertices = new List> +{ + new(0, 0), new(1, 0), new(1, 1), new(0, 1), +}; +var edges = new List +{ + new(0, 2), // diagonal ↗ + new(1, 3), // diagonal ↖ — intersects (0,2) +}; + +var cdt = new Triangulation( + VertexInsertionOrder.Auto, + IntersectingConstraintEdges.TryResolve, + minDistToConstraintEdge: 0.0); + +cdt.InsertVertices(vertices); +cdt.InsertEdges(edges); // intersection is resolved by inserting a new vertex +cdt.EraseSuperTriangle(); +``` + +## Building + +```bash +dotnet build +``` + +## Testing + +```bash +dotnet run --project test/CDT.Tests +``` + +## Benchmarking + +```bash +dotnet run -c Release --project benchmark/CDT.Benchmarks +``` + +## License + +[Mozilla Public License Version 2.0](LICENSE) + +This software is based in part on [CDT — C++ library for constrained Delaunay triangulation](https://github.com/artem-ogre/CDT): +Copyright © 2019 Leica Geosystems Technology AB +Copyright © The CDT Contributors +Licensed under the MPL-2.0 license. diff --git a/src/CDT.Core/CDT.Core.csproj b/src/CDT.Core/CDT.Core.csproj index ec4f631..efa7112 100644 --- a/src/CDT.Core/CDT.Core.csproj +++ b/src/CDT.Core/CDT.Core.csproj @@ -1,7 +1,7 @@ - net10.0 + net8.0;net10.0 enable enable true @@ -12,4 +12,28 @@ CDT + + + CDT.NET + 1.0.0 + Michael Conrad + Copyright © 2026 Michael Conrad + A fast and robust C# port of the artem-ogre/CDT Constrained Delaunay Triangulation library. Supports constrained edges, holes, and modern .NET 8+ optimizations. + CDT;constrained-delaunay;triangulation;geometry;mesh + https://github.com/MichaCo/CDT.NET + https://github.com/MichaCo/CDT.NET + git + MPL-2.0 + README.md + true + true + true + snupkg + portable + + + + + + diff --git a/test/CDT.Tests/CDT.Tests.csproj b/test/CDT.Tests/CDT.Tests.csproj index 8fc5c13..193f180 100644 --- a/test/CDT.Tests/CDT.Tests.csproj +++ b/test/CDT.Tests/CDT.Tests.csproj @@ -5,10 +5,13 @@ enable enable false + + true + true - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/CDT.Tests/ReadmeExamplesTests.cs b/test/CDT.Tests/ReadmeExamplesTests.cs new file mode 100644 index 0000000..1c30c6a --- /dev/null +++ b/test/CDT.Tests/ReadmeExamplesTests.cs @@ -0,0 +1,202 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +namespace CDT.Tests; + +/// Validates that the README code examples compile and produce the expected results. +public sealed class ReadmeExamplesTests +{ + // ------------------------------------------------------------------------- + // Example 1 – Delaunay triangulation without constraints (convex hull) + // ------------------------------------------------------------------------- + + [Fact] + public void Example_DelaunayConvexHull() + { + var vertices = new List> + { + new(0, 0), new(4, 0), new(4, 4), new(0, 4), new(2, 2), // inner point + }; + + var cdt = new Triangulation(); + cdt.InsertVertices(vertices); + cdt.EraseSuperTriangle(); // produces convex hull + + Assert.True(TopologyVerifier.VerifyTopology(cdt)); + Assert.Equal(5, cdt.Vertices.Count); + Assert.True(cdt.Triangles.Count > 0); + Assert.Empty(cdt.FixedEdges); + } + + // ------------------------------------------------------------------------- + // Example 2 – Constrained Delaunay triangulation (bounded domain) + // ------------------------------------------------------------------------- + + [Fact] + public void Example_ConstrainedDelaunay_BoundedDomain() + { + var vertices = new List> + { + new(0, 0), new(4, 0), new(4, 4), new(0, 4), + }; + var edges = new List + { + new(0, 1), new(1, 2), new(2, 3), new(3, 0), // square boundary + }; + + var cdt = new Triangulation(); + cdt.InsertVertices(vertices); + cdt.InsertEdges(edges); + cdt.EraseOuterTriangles(); // removes everything outside the boundary + + Assert.True(TopologyVerifier.VerifyTopology(cdt)); + Assert.Equal(4, cdt.Vertices.Count); + Assert.Equal(2, cdt.Triangles.Count); + Assert.Equal(4, cdt.FixedEdges.Count); + } + + // ------------------------------------------------------------------------- + // Example 3 – Auto-detect outer triangles and holes + // ------------------------------------------------------------------------- + + [Fact] + public void Example_AutoDetectBoundariesAndHoles() + { + // Outer square (vertices 0-3) + inner square hole (vertices 4-7) + var vertices = new List> + { + new(0, 0), new(6, 0), new(6, 6), new(0, 6), // outer square + new(2, 2), new(4, 2), new(4, 4), new(2, 4), // inner hole + }; + var edges = new List + { + // outer boundary (CCW) + new(0, 1), new(1, 2), new(2, 3), new(3, 0), + // inner hole boundary (CW — opposite winding marks it as a hole) + new(4, 7), new(7, 6), new(6, 5), new(5, 4), + }; + + var cdt = new Triangulation(); + cdt.InsertVertices(vertices); + cdt.InsertEdges(edges); + cdt.EraseOuterTrianglesAndHoles(); // removes outer AND fills holes automatically + + Assert.True(TopologyVerifier.VerifyTopology(cdt)); + Assert.Equal(8, cdt.Vertices.Count); + Assert.True(cdt.Triangles.Count > 0); + } + + // ------------------------------------------------------------------------- + // Example 4 – Conforming Delaunay triangulation + // ------------------------------------------------------------------------- + + [Fact] + public void Example_ConformingDelaunay() + { + var vertices = new List> + { + new(0, 0), new(4, 0), new(4, 4), new(0, 4), + }; + var edges = new List + { + new(0, 1), new(1, 2), new(2, 3), new(3, 0), + }; + + var cdt = new Triangulation(); + cdt.InsertVertices(vertices); + cdt.ConformToEdges(edges); // may split edges and add new points + cdt.EraseOuterTriangles(); + + Assert.True(TopologyVerifier.VerifyTopology(cdt)); + Assert.True(cdt.Triangles.Count > 0); + // ConformToEdges may have added midpoints, so vertex count >= 4 + Assert.True(cdt.Vertices.Count >= 4); + } + + // ------------------------------------------------------------------------- + // Example 5 – Removing duplicates and remapping edges + // ------------------------------------------------------------------------- + + [Fact] + public void Example_RemoveDuplicatesAndRemapEdges() + { + var vertices = new List> + { + new(0, 0), new(4, 0), new(4, 4), new(0, 4), + new(0, 0), // duplicate of vertex 0 + }; + var edges = new List + { + new(0, 4), // references duplicate; will be remapped to (0, 0) + new(1, 2), + }; + + CdtUtils.RemoveDuplicatesAndRemapEdges(vertices, edges); + + // Duplicate removed → 4 unique vertices + Assert.Equal(4, vertices.Count); + // Edge (0,4) remapped: both map to index 0 → self-edge (0,0) + Assert.Equal(new Edge(0, 0), edges[0]); + // Edge (1,2) unchanged + Assert.Equal(new Edge(1, 2), edges[1]); + + var cdt = new Triangulation(); + cdt.InsertVertices(vertices); + cdt.InsertEdges(edges.Where(e => e.V1 != e.V2).ToList()); // skip degenerate self-edge + cdt.EraseSuperTriangle(); + + Assert.True(TopologyVerifier.VerifyTopology(cdt)); + } + + // ------------------------------------------------------------------------- + // Example 6 – Extract all edges from a triangulation + // ------------------------------------------------------------------------- + + [Fact] + public void Example_ExtractEdgesFromTriangles() + { + var cdt = new Triangulation(); + cdt.InsertVertices([new(0, 0), new(2, 0), new(1, 2)]); + cdt.EraseSuperTriangle(); + + // Extract all unique edges from every triangle + HashSet allEdges = CdtUtils.ExtractEdgesFromTriangles(cdt.Triangles); + + // A single triangle has exactly 3 edges + Assert.Equal(3, allEdges.Count); + } + + // ------------------------------------------------------------------------- + // Example 7 – Resolve intersecting constraint edges + // ------------------------------------------------------------------------- + + [Fact] + public void Example_ResolveIntersectingConstraints() + { + // Two edges that cross each other: (0,2) and (1,3) on a unit square + var vertices = new List> + { + new(0, 0), new(1, 0), new(1, 1), new(0, 1), + }; + var edges = new List + { + new(0, 2), // diagonal + new(1, 3), // other diagonal — intersects (0,2) + }; + + // TryResolve splits intersecting edges by inserting the intersection point + var cdt = new Triangulation( + VertexInsertionOrder.Auto, + IntersectingConstraintEdges.TryResolve, + 0.0); + + cdt.InsertVertices(vertices); + cdt.InsertEdges(edges); + cdt.EraseSuperTriangle(); + + Assert.True(TopologyVerifier.VerifyTopology(cdt)); + // An extra vertex is added at the intersection + Assert.True(cdt.Vertices.Count > 4); + } +}