├── .codacy.yaml
├── .editorconfig
├── .github
└── workflows
│ └── dotnet-core.yml
├── .gitignore
├── Directory.Build.props
├── Documentation
├── AfterSingleSplit.png
├── BasicPolygon.png
├── Fusion1.png
├── Fusion2.png
├── Fusion3.png
├── Fusion4.png
├── FusionOrdering.png
├── FusionOrderingBad.png
├── FusionOrderingGood.png
├── Monotone1.png
├── Monotone2.png
├── Monotone3.png
├── Monotones.md
├── Polygon.md
├── PolygonTrapezoidLines.png
├── PolygonTrapezoidLinesFilled.png
├── PolygonTrapezoidLinesFilledSplits.png
├── SimpleHole.png
├── SimpleHoleJoiningSplit.png
├── SimpleHoleSplitted.png
├── SingleSplit.png
├── SplitAfterJoin.png
├── SplittedMonotones.png
└── Trapezoidation.md
├── LICENSE
├── PolygonDisplay
├── App.config
├── ExceptionHelper.cs
├── Form1.Designer.cs
├── Form1.cs
├── Form1.resx
├── PolygonController.cs
├── PolygonDisplay.csproj
├── PolygonDisplay.csproj.user
├── PolygonDrawControl.Designer.cs
├── PolygonDrawControl.cs
├── PolygonDrawControl.resx
├── PolygonSamples.cs
├── Program.cs
├── Properties
│ ├── AssemblyInfo.cs
│ ├── Resources.Designer.cs
│ ├── Resources.resx
│ ├── Settings.Designer.cs
│ └── Settings.settings
└── app.manifest
├── PolygonTriangulation.sln
├── PolygonTriangulation
├── PlanePolygonBuilder.ClusterVertexComparer.cs
├── PlanePolygonBuilder.EdgesToPolygonBuilder.cs
├── PlanePolygonBuilder.PlanePolygonData.cs
├── PlanePolygonBuilder.PolygonLine.cs
├── PlanePolygonBuilder.PolygonLineDetector.cs
├── PlanePolygonBuilder.TriangulatedPlanePolygon.cs
├── PlanePolygonBuilder.cs
├── Polygon.Builder.cs
├── Polygon.Extensions.cs
├── Polygon.NextChainEnumerable.cs
├── Polygon.Splitter.cs
├── Polygon.VertexChain.cs
├── Polygon.VertexInfo.cs
├── Polygon.cs
├── PolygonTriangulation.csproj
├── PolygonTriangulator.MonotonePolygonTriangulator.cs
├── PolygonTriangulator.ScanSplitByTrapezoidation.cs
├── PolygonTriangulator.TriangleCollector.cs
├── PolygonTriangulator.cs
├── Properties
│ └── AssemblyInfo.cs
├── RedBlackTree.DumpEnumerator.cs
├── RedBlackTree.Node.cs
├── RedBlackTree.cs
├── Trapezoidation.EdgeComparer.cs
├── Trapezoidation.Trapezoid.cs
├── Trapezoidation.TrapezoidEdge.cs
├── Trapezoidation.cs
└── TriangulationException.cs
├── Readme.md
└── TriangulationTests
├── EdgesToPolygonTests.cs
├── PolygonFusionTests.cs
├── PolygonSplittingTests.cs
├── Properties
└── AssemblyInfo.cs
├── RedBlackTreeTests.cs
├── TriangulationTests.csproj
├── UnitTest1.cs
└── packages.config
/.codacy.yaml:
--------------------------------------------------------------------------------
1 | engines:
2 | duplication:
3 | exclude_paths:
4 | - TriangulationTests/**
5 | - PolygonDisplay/PolygonSamples.cs
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cs]
2 |
3 | # IDE0065: Die using-Anweisung wurde falsch platziert.
4 | csharp_using_directive_placement = inside_namespace:silent
5 |
6 | # SA1629: Documentation text should end with a period
7 | dotnet_diagnostic.SA1629.severity = none
8 |
9 | # CA1303: Literale nicht als lokalisierte Parameter übergeben
10 | dotnet_diagnostic.CA1303.severity = none
11 |
12 | # SA1633: File should have header
13 | dotnet_diagnostic.SA1633.severity = none
14 |
15 | # CA1062: Check public parameters for null
16 | dotnet_diagnostic.CA1062.severity = none
17 |
18 | # SA1649: The first type should match the type name
19 | dotnet_diagnostic.SA1649.severity = none
20 |
21 | # CA1819: Properties may not return arrays
22 | dotnet_diagnostic.CA1819.severity = none
23 |
24 | # SA1300: object names
25 | dotnet_diagnostic.SA1300.severity = none
26 |
27 | # SA1300: type parameters
28 | dotnet_diagnostic.SA1619.severity = none
29 |
--------------------------------------------------------------------------------
/.github/workflows/dotnet-core.yml:
--------------------------------------------------------------------------------
1 | name: .NET Core
2 |
3 | on:
4 | push:
5 | branches: [ master, develop ]
6 | pull_request:
7 | branches: [ master, develop ]
8 | workflow_dispatch:
9 |
10 | jobs:
11 | build:
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Setup .NET Core
18 | uses: actions/setup-dotnet@v1
19 | with:
20 | dotnet-version: 3.1.101
21 | - name: Install dependencies
22 | run: dotnet restore TriangulationTests
23 | - name: Build
24 | run: dotnet build --configuration Release --no-restore TriangulationTests
25 | - name: Test
26 | run: dotnet test --no-restore --verbosity normal TriangulationTests /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
27 |
28 | - name: Upload Coverage to Codacy
29 | env: # Set the secret as an input
30 | CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
31 | run: bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r TriangulationTests/coverage.opencover.xml --commit-uuid $GITHUB_SHA
32 |
33 | - name: Setup .net core 5.0
34 | uses: actions/setup-dotnet@v1
35 | with:
36 | dotnet-version: 5.0.301
37 | - name: Install Coverage Tool
38 | # run: dotnet tool install --tool-path . coveralls.net
39 | run: dotnet tool install -g coveralls.net
40 | - name: Upload Coverage
41 | env: # Set the secret as an input
42 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
43 | run: csmacnz.Coveralls --opencover -i TriangulationTests/coverage.opencover.xml --useRelativePaths --commitId $GITHUB_SHA --commitBranch `echo $GITHUB_REF | sed -e 's#refs/heads/##'` --commitAuthor "$GITHUB_ACTOR" --commitMessage "$REPO_COMMIT_MESSAGE"
44 |
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vs
2 | */obj
3 | */bin
4 | packages
5 | Debug
6 | Bin
7 | *Tests/coverage.json
8 | *Tests/coverage.opencover.xml
9 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 | S1172,S3881
5 | ..\Bin\$(Configuration)
6 | ..\Bin\$(Configuration)\$(ProjectName).xml
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Documentation/AfterSingleSplit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/AfterSingleSplit.png
--------------------------------------------------------------------------------
/Documentation/BasicPolygon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/BasicPolygon.png
--------------------------------------------------------------------------------
/Documentation/Fusion1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/Fusion1.png
--------------------------------------------------------------------------------
/Documentation/Fusion2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/Fusion2.png
--------------------------------------------------------------------------------
/Documentation/Fusion3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/Fusion3.png
--------------------------------------------------------------------------------
/Documentation/Fusion4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/Fusion4.png
--------------------------------------------------------------------------------
/Documentation/FusionOrdering.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/FusionOrdering.png
--------------------------------------------------------------------------------
/Documentation/FusionOrderingBad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/FusionOrderingBad.png
--------------------------------------------------------------------------------
/Documentation/FusionOrderingGood.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/FusionOrderingGood.png
--------------------------------------------------------------------------------
/Documentation/Monotone1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/Monotone1.png
--------------------------------------------------------------------------------
/Documentation/Monotone2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/Monotone2.png
--------------------------------------------------------------------------------
/Documentation/Monotone3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/Monotone3.png
--------------------------------------------------------------------------------
/Documentation/Monotones.md:
--------------------------------------------------------------------------------
1 | # Monotone Polygons
2 |
3 | A monotone [polygon](Polygon.md) is intersected by any vertical line at maxium 2 times.
4 |
5 | A mono monotone polygon has a single edge that connects the left-most and the right-most vertex.
6 | Call it the __base edge__.
7 | The other vertices are either all above or all below the base edge.
8 |
9 | After splitting a polygon via [Trapezoidation](Trapezoidation.md), the polygon consists of multiple mono monotone sub polygons.
10 |
11 |
12 |
13 | ## Triangulation of Mono Monotone Polygons
14 |
15 | `MonotonePolygonTriangulator` starts at the end vertex of base edge. That is vertex 7 of polygon A and vertex 5 of polygon B.
16 | * Take the first three vertices.
17 | * Iterate to the end
18 | * If the three active vertices do not form a clock wise triangle (the cross product of two triangle sides is >= 0)
19 | * Push the oldest vertex on a stack and get a new vertex.
20 | * Else
21 | * Emit the triangle.
22 | * Remove the middle vertex.
23 | * If there is a vertex on the stack, pop it as oldest vertex.
24 | * Otherwise get a new vertex.
25 | * If there is no new vertex, the triangulation is complete.
26 |
27 |
28 |
29 | Example A:
30 | * Start at 7, after initial pull: 7, 6, 3
31 | * (7, 6, 3) is a clock wise triangle, emit and remove 6.
32 | * Take the oldest (7) and newest (3) vertex and pull a new one (2)
33 | * (7, 3, 2) is a clock wise triangle
34 |
35 | Example B:
36 | * Start at 5, after initial pull: 5, 6, 7
37 | * It's no clock wise triangle, so push 5 on the stack and take 8 as newest vertex.
38 | * (6, 7, 8) is a clock wise triangle, emit and remove 7.
39 | * There is (5) on the stack, use it as oldest
40 | * (5, 6, 8) is a clock wise triangle, emit and remove 6.
41 | * Take the oldest (5) and newest (8) vertex and pull a new one (9)
42 | * (5, 8, 9) is a clock wise triangle
43 |
44 |
45 |
46 | A more complex example:
47 | * Start at 1, after initial pull: 1, 3, 4
48 | * (1, 3, 4) is no clock wise triangle, push 1 and consume 5
49 | * (3, 4, 5) is no clock wise triangle, push 3 and consume 6
50 | * (4, 5, 6) is a clock wise triangle. Emit, remove 5 and pop 3 as oldest
51 | * (3, 4, 6) is a clock wise triangle. Emit, remove 4 and pop 1 as oldest
52 | * (1, 3, 6) is no clock wise triangle, push 1 and consume 7
53 | * (3, 6, 7) is no clock wise triangle, push 3 and consume 8
54 | * (6, 7, 8) is a clock wise triangle. Emit, remove 7 and pop 3 as oldest
55 | * (3, 6, 8) is a clock wise triangle. Emit, remove 6 and pop 1 as oldest
56 | * (1, 3, 8) is a clock wise triangle. Emit, remove 3 and consume 9
57 | * (1, 8, 9) is no clock wise triangle, push 1 and consume 13
58 | * (8, 9, 13) is a clock wise triangle. Emit, remove 9 and pop 1 as oldest
59 | * (1, 8, 13) is a clock wise triangle. Emit, remove 8 and consume 16
60 | * (1, 13, 16) is a clock wise triangle. Emit and stop after last vertex.
61 |
62 | ## Optimization for triangles
63 |
64 | A lot of splits produce sub polygons with 3 vertices, aka triangles.
65 | The polygon split operation simply emits those directly instead of creating an additional sub polygon.
66 |
--------------------------------------------------------------------------------
/Documentation/Polygon.md:
--------------------------------------------------------------------------------
1 | # Polygon
2 |
3 | A polygon holds a number of vertices, connnected by edges that enclose the polygon area.
4 | During the [Trapezoidation](Trapezoidation.md), the polygon is split into multiple [monotone polygons](Monotones.md).
5 |
6 | A polygon can contain multiple polygon lines, called __sub polygons__.
7 | The enclosed areas can be side by side or they define a hole inside another polygon area.
8 | The edges of the polygon must not intersect.
9 |
10 | Each edge is directed, the polygon area is always to the right of the edge.
11 | Hence the vertices are connected in clock wise order and holes in counter clock wise order.
12 |
13 |
14 |
15 | ## Generating a polygon from edges
16 |
17 | The `PlanePolygonBuilder` creates a polygon from edges on a 3D plane.
18 | The vertices are first rotated to the plane normal and treated as 2D coordinates afterwards.
19 |
20 | Very close vertices are considered the same to compensate rounding issues. That happens during the sort of the vertices.
21 |
22 | Edges, that are not part of a closed polygon, are listed in `IPolygonLineDetector.UnclosedPolygons`.
23 | The top level call of the library, `PlanePolygonBuilder.Build`, simply ignores unclosed polygons.
24 |
25 | ## Splitting a polygon
26 |
27 | The core operation for the polygon is to be splitted into multiple sub polygons.
28 | The initial polygon contains a single sub polygon: 0 1 2 5 4 3.
29 | After the split between 2 and 3, there are 2 sub polygons: 0 1 2 3 and 2 5 4 3.
30 | The vertices 2 and 3 belong to two sub polygons.
31 |
32 |
33 |
34 | A vertex can be the target of multiple splits, so it will be part of more than 2 _sub polygons_.
35 | There is always exactly one _sub polygon_, which contains both vertices of a split.
36 | `FindCommonChain` selects the correct instance for both vertices.
37 |
38 | ### Handling of holes
39 |
40 | A hole is a special case of a split. It's easily detected because the two vertices belong to different sub polygons.
41 | The `JoinHoleIntoPolygon` creates two edges, the first connects from 1 to 2 and the second connects from 2' to 1'.
42 | The resulting single sub polygon is defined by 0 1 2 3 5 4 2' 1' 7 6.
43 |
44 | The second split 5-6 splits that polygon again into two sub polygons: 0 1 2 3 5 6 and 1' 7 6 5 4 2'.
45 |
46 | All splits, that join a hole into another sub polygon are processed first.
47 |
48 |
49 |
50 | ### Combination of splits and holes at a single vertex
51 |
52 | For the next polygon, the [Trapezoidation](Trapezoidation.md) finds the splits 1-2, 2-4, 4-5 and 6-7.
53 | The first split, 1-2, joins the hole into the polygon.
54 |
55 | The next split 2-4 can start either at __2__ or __2'__.
56 | Splitting along 2'-4 creates the correct sub polygons 1' 4 2' and 0 1 2 3 6 5 2' 4 8 7.
57 | Splitting along 2-4 creates the wrong sub polygons 1' 4 2 3 6 5 2' and 0 1 2' 4 8 7.
58 | These sub polygons overlap each other, causing later processing errors.
59 |
60 | `ChooseInstanceForSplit` chooses the correct vertex __2__ between _5 __2'__ 1'_ and _1 __2__ 3_.
61 | The new edge must be between the incoming edge and the outgoing edge,
62 | e.g. 2'-4 is between 2'-5 and 2'-1' but 2-4 is not between 2-1 and 2-3.
63 |
64 |
65 |
66 | ## Internal structure of a polygon
67 |
68 | The 2D vertices are stored in the array `Polygon.Vertices`. The array is never modified.
69 | The vertices must be sorted from left to right and then from bottom to top.
70 |
71 | The polygon outline of a _sub polygon_ is represented by a chain of _vertex instances_.
72 | The `chain` is an array of type `Polygon.VertexChain` and can contain multiple instances of the same Vertex:
73 | * The `VertexId`, defined as array index in `Polygon.Vertices`.
74 | * The `Prev` and `Next` chain element in the current _sub polygon_, indices in `chain`.
75 | * The `SubPolygonId` of the _vertex instance_.
76 | * The `SameVertexChain` points to the next _vertex instance_ with the same `VertexId` but a different `SubPolygonId`.
77 |
78 | The `vertexToChain` maps the _vertex id_ to the index in the vertex `chain`.
79 |
80 | The _sub polygons_ store the index of one chain element (aka _vertex instance_) in `polygonStartIndices`.
81 | The index in that array is the _sub polygon id_, which is used by `Polygon.SubPolygonIds` and `VertexChain.SubPolygonId`.
82 | The vertex list of a _sub polygon_ is available by `Polygon.SubPolygonVertices(int subPolygonId)`.
83 |
84 | ## Crafting a polygon for unittests
85 |
86 | Polygons can be created by `Polygon.Build`.
87 | The vertices must be sorted in X and then in Y direction and they're added in clockwise order.
88 | The vertex id is the index in the `sortedVertices` array:
89 | ``` csharp
90 | var sortedVertices = new[]
91 | {
92 | new Vertex(0, 0),
93 | new Vertex(0, 1),
94 | new Vertex(1, 0),
95 | new Vertex(1, 1),
96 | };
97 |
98 | var polygon = Polygon.Build(sortedVertices)
99 | .AddVertices(0, 1, 3, 2)
100 | .Close();
101 | ```
102 |
103 | A polygon can contains multiple side by side polygons or even holes. The vertices of a hole must be connected in counter-clockwise order:
104 | ``` csharp
105 | var polygon = Polygon.Build(sortedVertices)
106 | .AddVertices(1, 0, 2, 5, 8, 6)
107 | .ClosePartialPolygon()
108 | .AddVertices(3, 4, 7)
109 | .Close();
110 | ```
111 |
112 | A fusion vertex can join two or more polygons in a common vertex. Those must be mentioned during `Close()`:
113 |
114 | ``` csharp
115 | var polygon = Polygon.Build(sortedVertices)
116 | .AddVertices(0, 1, 2)
117 | .ClosePartialPolygon()
118 | .AddVertices(2, 3, 4)
119 | .ClosePartialPolygon()
120 | .AddVertices(4, 5, 6)
121 | .Close(2, 4);
122 | ```
123 |
124 | ## Fusion vertices
125 |
126 | Sometimes the very same vertex is used in two different sub polygons. The two polygons __fusion__ in that point.
127 | That can be two separate polygons or a hole in the polygon. The same two polygons can even touch multiple times.
128 |
129 |
130 |
131 |
132 | ### Fusion vertices during polygon building
133 | The polygon outline must never cross itself.
134 | During polygon construction, `FusionVerticesIntoChain` sorts the edges around each fusion point in clockwise order.
135 | A valid polygon order is 0 2 __5__ 1 3 __5__ 4 6 __5__ 7 8 __5__ 9 10.
136 |
137 |
138 |
139 | But it would be invalid to use 0 2 __5__ 4 6 __5__ 1 3 __5__ 7 8 __5__ 9 10, because 2 5 4 crosses 3 5 7.
140 |
141 |
142 |
143 | ### Fusion vertices during Trapezoidation
144 | The [Trapezoidation](Trapezoidation.md) will process a fusioned vertex multiple times.
145 | The processing order is "Joining cusps", "Transitions", "Opening Cusps", which resembles the left to right processing of the vertices.
146 |
147 |
148 |
149 | A bad example: consider processing the opening cusp 6-4-7 _before_ the transition 2-4-5:
150 | * Order of the active edges before 6-4-7 from high to low: 2-4 0-8.
151 | * 4-6 is inserted and considered to be below 2-4 and above 0-8.
152 | * Active edges after inserting the cusp: 2-4 4-7 4-6 0-8.
153 | * The transition from 2-4 to 4-5 just replaces 2-4 by 4-5. (A transition expects to stay on the same level.)
154 | * Active edges after transition: 4-5 4-7 4-6 0-8. => Corrupted.
155 |
156 | > The problem effectively arises because the vertex 4 (left vertex of the inserted edge) is compared to edge 2-4.
157 | > But it's neither above nor below.
158 | >
159 | > Processing all closing cusps and transitions before the insert operation ensures, that there is no active edge having the fusion vertex on the right.
160 |
161 | Processing the transition before the opening cusp, edge 4-6 is compared to 4-5.
162 | The comparer detects that both edges have the same left vertex and chooses the right vertex 6 for comparison.
163 |
--------------------------------------------------------------------------------
/Documentation/PolygonTrapezoidLines.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/PolygonTrapezoidLines.png
--------------------------------------------------------------------------------
/Documentation/PolygonTrapezoidLinesFilled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/PolygonTrapezoidLinesFilled.png
--------------------------------------------------------------------------------
/Documentation/PolygonTrapezoidLinesFilledSplits.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/PolygonTrapezoidLinesFilledSplits.png
--------------------------------------------------------------------------------
/Documentation/SimpleHole.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/SimpleHole.png
--------------------------------------------------------------------------------
/Documentation/SimpleHoleJoiningSplit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/SimpleHoleJoiningSplit.png
--------------------------------------------------------------------------------
/Documentation/SimpleHoleSplitted.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/SimpleHoleSplitted.png
--------------------------------------------------------------------------------
/Documentation/SingleSplit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/SingleSplit.png
--------------------------------------------------------------------------------
/Documentation/SplitAfterJoin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/SplitAfterJoin.png
--------------------------------------------------------------------------------
/Documentation/SplittedMonotones.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-ruttmann/PolygonTriangulation/5521b9858cc4a20c307ca13f4b3a19dddf3a91de/Documentation/SplittedMonotones.png
--------------------------------------------------------------------------------
/Documentation/Trapezoidation.md:
--------------------------------------------------------------------------------
1 | # Trapezoidation
2 |
3 | The area of the [polygon](Polygon.md) can be splitted into multiple sub polygons, called [Monotone Polygons](Monotones.md).
4 | Monotones can be splitted into triangles easily.
5 |
6 | ## The example polygon
7 |
8 |
9 |
10 | ## Names
11 |
12 | These names distinguish between polygon elements and trapezoid elements:
13 | * A __vertex__ is a polygon point.
14 | * An __edge__ is a polygon line.
15 | * A __corner__ is a trapezoid point.
16 | * A __side__ is a trapezoid line.
17 | * A __base__ is one of the two parallel trapezoid sides.
18 |
19 | ## Creating trapezoids
20 |
21 | From each vertex, draw a line up to the next edge and down to the next edge. Imagine an upper infinity edge and a lower infinity edge.
22 |
23 |
24 |
25 | __Characteristics of a trapezoid__:
26 | * A trapezoid has 4 corners defining 4 sides.
27 | * The left and the right base are vertical and parallel. They are not part of the polygon.
28 | * Each base is defined by the X component of a vertex.
29 | * The top and the bottom sides are part of a polygon edge.
30 | * So each Trapezoid is defined by a left vertex, a right vertex, a top edge and a bottom edge.
31 | * The area that's enclosed by the 4 sides is never crossed by an edge.
32 | * Sometimes one trapezoid base has length 0, so it represents a triangle.
33 |
34 | ## Splitting the polygon
35 |
36 | Here are all created trapezoids, each with a different color. The outside trapezoids are yellow, the inside trapezoids are grey.
37 |
38 |
39 |
40 | The vertex of a trapezoid base may have 4 different positions, see `Trapezoidation.Trapezoid.Base`:
41 | * It's equal to the upper corner of the base, e.g. Vertex 2 on the left side of Trapezoid __B__. (`UpperCorner`).
42 | * It's equal to the lower corner of the base, e.g. Vertex 3 on the right side of Trapezoid __B__. (`LowerCorner`).
43 | * It's in the middle of the edge, e.g. vertex 2 on the right side of Trapezoid __A__. (`TwoNeighbors`).
44 | * The top and the bottom corner is the same, e.g. the left side of Trapezoid __A__. (`NoNeighbor`).
45 |
46 | As no polygon edge passes _through_ the area of a trapezoid, it's safe to split the polygon along the two vertices.
47 | The following rules detect if a split does not duplicate an existing edge:
48 | 1. One trapezoid base has two neighbors, i.e. the base is touched by a cusp of two polygon edges.
49 | 2. The vertex of one base is on the upper edge (`UpperCorner`) and the other is on the lower edge (`LowerCorner`).
50 |
51 | Applying the rules to the example:
52 | * Trapezoid __A__ has `TwoNeighbors` on the right base. Split from vertex 0 to 2.
53 | * Trapezoid __B__ has `UpperCorner` on the left base and `LowerCorner` on the right base. Split from vertex 2 to 3.
54 | * Trapezoid __C__ has `LowerCorner` on the left base and `LowerCorner` on the right base. No split.
55 | * Trapezoid __D__ has `TwoNeighbors` on the left base. Split from vertex 6 to 7.
56 | * Trapezoid __E__ has `TwoNeighbors` on the right base. Split from vertex 7 to 8.
57 | * Trapezoid __F__ has `UpperCorner` on the left base and `LowerCorner` on the right base. Split from vertex 8 to 9.
58 | * Trapezoid __G__ has `LowerCorner` on the left base and `NoNeighbor` on the right base. No split.
59 | * Trapezoid __H__ has `LowerCorner` on the left base and `UpperCorner` on the right base. Split from vertex 2 to 4.
60 | * Trapezoid __I__ has `UpperCorner` on the left base and `NoNeighbor` on the right base. No split.
61 | * Trapezoid __J__ has `NoNeighbor` on the left base and `LowerCorner` on the right base. No split.
62 | * Trapezoid __K__ has `LowerCorner` on the left base and `UpperCorner` on the right base. Split from vertex 5 to 6.
63 | * Trapezoid __L__ has `LowerCorner` on the left base and `NoNeighbor` on the right base. No split.
64 |
65 | The polygon with all possible splits:
66 |
67 |
68 |
69 | And finally the [Monotone Polygons](Monotones.md), simple triangles are grey:
70 |
71 |
72 |
73 | ## Assumptions for the Implementation
74 |
75 | * The polygon edges are directed.
76 | * The outer polygon is in _clock wise_ order, holes are in _counter clock wise_ order.
77 | * Left of an edge is outside the polygon, right of an edge is inside of the polygon.
78 | * The vertices are sorted from left to right and from bottom to top.
79 |
80 | For the split detection, only the inside trapezoids are necessary.
81 | For each X coordinate, there is exactly one inside trapezoid per edge.
82 | As the vertices are traversed from left to right, the edge can be used to store and look up the trapezoid.
83 |
84 | ## Splitting programmatically
85 |
86 | The algorithm iterates over the vertices in X order (`Polygon.OrderedVertices`).
87 | Each vertex knows the incoming edge and the outgoing edge.
88 | That leaves us with 6 situations:
89 | * Both neighbor vertices are larger, so it's a left pointing cusp. __(O)__
90 | * Vertex 0, prev is 3, next is 4. The incoming edge is the lower one, so we're entering the polygon area. __(1)__ `Trapezoid.EnterInsideBySplit`
91 | * Vertex 2, prev is 11, next is 7. The incoming edge is the upper one, so we're leaving the polygon area. __(2)__ `Trapezoid.LeaveInsideBySplit`
92 | * One neighbor vertex is larger, the other one is smaller, so the polygon transitions from one edge to the next. __(T)__
93 | * Vertex 5, prev is 9 (larger), next is 1 (smaller). The edges are right to left, the polygon area is above __(3)__. `Trapezoid.TransitionOnLowerEdge`
94 | * Vertex 4, prev is 0 (smaller), next is 11 (larger). The edges are left to right, the polygon area is below __(4)__. `Trapezoid.TransitionOnUpperEdge`
95 | * Both neighbor vertices are smaller, so it's a right pointing cusp. __(J)__
96 | * Vertex 6, prev is 1, next is 3. The incoming edge is the lower one, so we're entering the polygon area. __(5)__ `Trapezoid.EnterInsideByJoin`
97 | * Vertex 10, prev is 8, next is 9. The incoming edge is the upper one, so we're leaving the polygon area. __(6)__ `Trapezoid.LeaveInsideByJoin`
98 |
99 | While iterating over the vertices, there is a list `Trapezoidation.activeEdges`, sorted from bottom to top.
100 | That structure allows to find the previous (lower) and the next (upper) of an edge.
101 | Each pair of active edges points to one trapezoid, one as upper side and one as lower side.
102 | E.g. before we process vertex 6, the active edges are 5-9(__K__), 1-6(__K__), 3-6(__C__), 2-7(__C__), 2-11(__I__), 4-11(__I__).
103 | * Situation __(O)__ first compares the two edges to distinguish situation __(1)__ and __(2)__.
104 | * Insert the pair in `activeEdges`.
105 | * Situation __(1)__ creates one trapezoid like __A__ or __J__. The two active edges point to the new trapezoid. The left base is set to `NoNeighbor`.
106 | * In Situation __(2)__ there is an edge above the upper and one below the lower, both point to the left trapezoid (__A__)
107 | * Close the trapezoid to the left and set the right edge to `TwoNeighbors`.
108 | * Create a trapezoid between upper and upper.above (__H__).
109 | * Create a trapezoid between lower and lower.below (__B__).
110 | * During a transition __(T)__, the previous edge is part of `activeEdges` and that points to a trapezoid.
111 | * Close the old trapezoid and set the right base to `UpperCorner` or `LowerCorner`, depending on if the edge is pointing left or right.
112 | * Create a new trapezoid with the new active edge and the peer edge.
113 | * Replace the active edge with the new active edge.
114 | * e.g. Transition at point 5: close __J__ with `LowerCorner` as right base.
115 | Create __K__ with the new edge 5-9 as lower and the above of 1-5, which is 1-6.
116 | In `activeEdges`, replace 1-5 with 5-9.
117 | * Situation __(J)__ joines two existing active edges. Both must have the same right vertex. If the lower edge direction is left to right, it's situation __(5)__.
118 | * In Situation __(5)__:
119 | * Close the trapezoid of the upper edge (__C__) with `LowerCorner` as right base.
120 | * Close the trapezoid of the lower edge (__K__) with `UpperCorner` as right base.
121 | * Create a new trapezoid between upper.Above and lower.Below. Set the left base to `TwoNeighbors`.
122 | * Remove the two edges from `activeEdges`.
123 | * In Situation __(6)__, the upper edge and lower edge both point to the same trapezoid (__G__).
124 | Set the right base to `NoNeighbor` and remove both edges from `activeEdges`.
125 |
126 | ## Vertical polygon edges
127 |
128 | The upper vertex of the polygon edge is considered to be "slightly right" to the lower point of the vertical polygon edge.
129 | Thats why the points are sorted not only by their X coordinate but also by the Y coordinate.
130 |
131 | The vertical _polygon edge_ is treated as a horizontal _trapezoid side_.
132 |
133 | ## Performance considerations
134 |
135 | The `activeEdges` is implemented as a red black tree. That's a self-balancing binary tree.
136 |
137 | The number of insert operations can be reduced:
138 | * Edges are inserted pairwise. The first edge is the insert position for the second edge, as it's exactly above the first edge.
139 | * The insert operation returns the internal node as `IOrderedNode`. That's reused for replace and remove operations.
140 | * A node can be replaced with different data instead of two remove/insert operations.
141 | * The default delete operation of a red black tree replaces the data of a node.
142 | This implementation replaces the node itself. Thats important because somebody might hold a reference to the "replace with" node.
143 | * For lookup, only the Prev() and Next() is required. There is no lookup by data.
144 |
145 | To find the previous edge during a Transition __(T)__ or both joining edges of a right pointing cusp __(J)__,
146 | all active edges are stored in a hashseet with the right vertex as key.
147 | (Actually it uses the index of the polygon chain for that vertex, as the same vertex may be used by multiple sub polygons).
148 |
149 | ## Comparing edges
150 |
151 | Edges are compared by `Trapezoidation.EdgeComparer` for an opening cusp and during the insert operation in `activeEdges`.
152 | * For an opening cusp, the left point is the same.
153 | * The slow path calculates the Y of one edge at the X of the right point of the other edge.
154 | This implies that the edges of the polygon never intersect.
155 | * An edge may be vertical. To avoid rounding issues with high slopes, the point is calculated for the edge with the larger X span.
156 | * The implementation avoids calculation wherever it's unnecessary.
157 | E.g. the left point is the same and the right1.Y is above and right2.Y is below left.Y.
158 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Matthias Ruttmann
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/PolygonDisplay/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/PolygonDisplay/ExceptionHelper.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonDisplay
2 | {
3 | using System;
4 |
5 | ///
6 | /// exception handling helper
7 | ///
8 | public static class ExceptionHelper
9 | {
10 | ///
11 | /// Determines whether the exception can be swallowed.
12 | ///
13 | /// The exception.
14 | /// true if the exception is not too severe
15 | public static bool CanSwallow(Exception exception)
16 | {
17 | if (exception is OutOfMemoryException)
18 | {
19 | return false;
20 | }
21 |
22 | return true;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/PolygonDisplay/Form1.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonDisplay
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Drawing;
6 | using System.Linq;
7 | using System.Text.RegularExpressions;
8 | using System.Windows.Forms;
9 |
10 | using PolygonTriangulation;
11 |
12 | ///
13 | /// A simple form to select polygons and host a polygon draw control
14 | ///
15 | public partial class Form1 : Form, IPolygonForm
16 | {
17 | private readonly PolygonController controller;
18 |
19 | ///
20 | /// Initializes a new instance of the class.
21 | ///
22 | public Form1()
23 | {
24 | this.InitializeComponent();
25 | this.controller = new PolygonController(this);
26 | this.ActivateStorage(1);
27 | }
28 |
29 | ///
30 | /// Refreshes the state.
31 | ///
32 | public void RefreshState()
33 | {
34 | this.UpdateSelectedBufferState();
35 | this.polygonOrderLabel.Text = string.Join(" ", this.controller.Polygon.Debug);
36 | this.vertexIdSelector.Maximum = this.controller.Polygon.Vertices.Count() - 1;
37 | this.vertexIdSelector.Value = Math.Max(0, Math.Min(this.vertexIdSelector.Maximum, this.polygonPanel.HighlightIndex));
38 | }
39 |
40 | ///
41 | /// Gets the id of the storage button by it's name
42 | ///
43 | /// The sender.
44 | /// the id of the selected storage
45 | private static int IdOfStorageButton(object sender)
46 | {
47 | return int.Parse((sender as Control).Name.Substring(1));
48 | }
49 |
50 | ///
51 | /// Handles the Click event of the storage control.
52 | ///
53 | /// The source of the event.
54 | /// The instance containing the event data.
55 | private void storage_Click(object sender, EventArgs e)
56 | {
57 | var id = IdOfStorageButton(sender);
58 | this.ActivateStorage(id);
59 | }
60 |
61 | ///
62 | /// Activates the storage.
63 | ///
64 | /// The identifier.
65 | private void ActivateStorage(int id)
66 | {
67 | this.controller.ActivateStorage(id);
68 | this.polygonPanel.Polygon = this.controller.Polygon;
69 | this.polygonPanel.Splits = this.controller.Splits;
70 | this.vertexText.Text = string.Join(
71 | Environment.NewLine,
72 | this.controller.Polygon.OrderedVertices
73 | .Select(x => $"{x.PrevVertexId}>{x.Id}>{x.NextVertexId}")
74 | .ToArray());
75 |
76 | if ((Control.ModifierKeys & Keys.Shift) == 0)
77 | {
78 | this.polygonPanel.AutoScale();
79 | }
80 |
81 | this.RefreshState();
82 | }
83 |
84 | ///
85 | /// Handles the Click event of the button1 control.
86 | ///
87 | /// The source of the event.
88 | /// The instance containing the event data.
89 | private void button1_Click(object sender, EventArgs e)
90 | {
91 | this.vertexText.Lines = this.controller.BuildTrapezoidationDebug();
92 | }
93 |
94 | ///
95 | /// Updates the selected state for the selected buffer.
96 | ///
97 | private void UpdateSelectedBufferState()
98 | {
99 | var buttons = this.storagePanel.Controls
100 | .OfType()
101 | .Where(x => Regex.Match(x.Name, "^s[0-9*]$").Success);
102 | foreach (var button in buttons)
103 | {
104 | button.BackColor = IdOfStorageButton(button) == this.controller.ActiveStorageId ? SystemColors.MenuHighlight : this.debugButton.BackColor;
105 | }
106 | }
107 |
108 | ///
109 | /// Handles the Scroll event of the zoomSlider control.
110 | ///
111 | /// The source of the event.
112 | /// The instance containing the event data.
113 | private void zoomSlider_Scroll(object sender, EventArgs e)
114 | {
115 | this.polygonPanel.Zoom = this.zoomSlider.Value;
116 | }
117 |
118 | ///
119 | /// Handles the Scroll event of the vertexIdSelector control.
120 | ///
121 | /// The source of the event.
122 | /// The instance containing the event data.
123 | private void vertexIdSelector_Scroll(object sender, EventArgs e)
124 | {
125 | this.polygonPanel.HighlightIndex = this.vertexIdSelector.Value;
126 | if ((Control.ModifierKeys & Keys.Shift) == 0)
127 | {
128 | this.polygonPanel.CenterOnHighlight();
129 | }
130 |
131 | this.vertexIdLabel.Text = this.vertexIdSelector.Value.ToString();
132 | }
133 |
134 | ///
135 | /// Handles the Click event of the splitButton control.
136 | ///
137 | /// The source of the event.
138 | /// The instance containing the event data.
139 | private void splitButton_Click(object sender, EventArgs e)
140 | {
141 | try
142 | {
143 | this.polygonPanel.Splits =
144 | new PolygonTriangulator(this.controller.Polygon).GetSplits().ToArray();
145 | }
146 | catch (Exception ex)
147 | {
148 | if (!ExceptionHelper.CanSwallow(ex))
149 | {
150 | throw;
151 | }
152 |
153 | this.vertexText.Text = ex.ToString();
154 | }
155 |
156 | this.polygonPanel.AutoScale();
157 | }
158 |
159 | ///
160 | /// Handles the Click event of the triangulateButton control.
161 | ///
162 | /// The source of the event.
163 | /// The instance containing the event data.
164 | private void triangulateButton_Click(object sender, EventArgs e)
165 | {
166 | this.vertexText.Text = string.Empty;
167 | var lines = new List();
168 | var collector = PolygonTriangulator.CreateTriangleCollector();
169 | try
170 | {
171 | var triangulator = new PolygonTriangulator(this.controller.Polygon);
172 | var splits = triangulator.GetSplits();
173 | lines.Add("Splits");
174 | lines.AddRange(splits.Select(x => $"{x.Item1} - {x.Item2}"));
175 | lines.Add(string.Empty);
176 |
177 | var monotones = Polygon.Split(this.controller.Polygon, splits, PolygonTriangulator.CreateTriangleCollector());
178 | lines.Add("Monotones");
179 | lines.AddRange(monotones.SubPolygonIds.Select(x => string.Join(" ", monotones.SubPolygonVertices(x))));
180 | lines.Add(string.Empty);
181 |
182 | triangulator.BuildTriangles(collector);
183 | }
184 | catch (Exception ex)
185 | {
186 | if (!ExceptionHelper.CanSwallow(ex))
187 | {
188 | throw;
189 | }
190 |
191 | this.vertexText.Text = ex.ToString();
192 | }
193 |
194 | var triangles = collector.Triangles;
195 | lines.Add("Triangles");
196 | for (int i = 0; i < triangles.Length; i += 3)
197 | {
198 | lines.Add($"{triangles[i + 0]} {triangles[i + 1]} {triangles[i + 2]} ");
199 | }
200 |
201 | this.vertexText.Text += string.Join(Environment.NewLine, lines);
202 | this.polygonPanel.AutoScale();
203 | }
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/PolygonDisplay/Form1.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
--------------------------------------------------------------------------------
/PolygonDisplay/PolygonController.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonDisplay
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 |
7 | using PolygonTriangulation;
8 |
9 | ///
10 | /// the necessary abstraction for the form
11 | ///
12 | public interface IPolygonForm
13 | {
14 | ///
15 | /// Refresh all state dependent GUI elements.
16 | ///
17 | void RefreshState();
18 | }
19 |
20 | ///
21 | /// A controller for the polygon behaviour
22 | ///
23 | public class PolygonController
24 | {
25 | private readonly IPolygonForm form;
26 |
27 | ///
28 | /// Initializes a new instance of the class.
29 | ///
30 | /// The form.
31 | public PolygonController(IPolygonForm form)
32 | {
33 | this.form = form;
34 | this.ActiveStorageId = 1;
35 |
36 | PolygonSamples.GenerateDataOne();
37 | }
38 |
39 | ///
40 | /// Gets the id of the actived storage.
41 | ///
42 | public int ActiveStorageId { get; private set; }
43 |
44 | ///
45 | /// Gets the polygon to draw.
46 | ///
47 | public Polygon Polygon { get; private set; }
48 |
49 | ///
50 | /// Gets the splits of the polygon.
51 | ///
52 | public IReadOnlyCollection> Splits { get; private set; }
53 |
54 | ///
55 | /// Activates the storage.
56 | ///
57 | /// The identifier.
58 | public void ActivateStorage(int id)
59 | {
60 | this.ActiveStorageId = id;
61 |
62 | //// this.Polygon = PolygonSamples.Constructed(id);
63 | //// this.Polygon = PolygonSamples.UnityErrors(id);
64 | //// this.Polygon = PolygonSamples.InnerFusionSingle(id);
65 | //// this.Polygon = PolygonSamples.MultiTouch(id);
66 | this.Polygon = PolygonSamples.MoreFusionTests(id);
67 |
68 | try
69 | {
70 | this.Splits = new PolygonTriangulator(this.Polygon).GetSplits().ToArray();
71 | }
72 | catch (Exception e)
73 | {
74 | if (!ExceptionHelper.CanSwallow(e))
75 | {
76 | throw;
77 | }
78 |
79 | this.Splits = new Tuple[0];
80 | }
81 | }
82 |
83 | ///
84 | /// Build debug steps for the trapezoidation.
85 | ///
86 | /// the debug text
87 | public string[] BuildTrapezoidationDebug()
88 | {
89 | var lines = new List();
90 | var trapezoidation = new PolygonTriangulator(this.Polygon);
91 | int limit = 0;
92 | foreach (var vertexInfo in this.Polygon.OrderedVertices)
93 | {
94 | try
95 | {
96 | lines.Add($"{vertexInfo.PrevVertexId}>{vertexInfo.Id}>{vertexInfo.NextVertexId}");
97 | lines.AddRange(trapezoidation.GetEdgesAfterPartialTrapezoidation(++limit));
98 | lines.Add(string.Empty);
99 | }
100 | catch (Exception e)
101 | {
102 | if (!ExceptionHelper.CanSwallow(e))
103 | {
104 | throw;
105 | }
106 |
107 | lines.Add(string.Empty);
108 | lines.Add(e.ToString());
109 | break;
110 | }
111 | }
112 |
113 | return lines.ToArray();
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/PolygonDisplay/PolygonDisplay.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netcoreapp3.1
4 | WinExe
5 | publish\
6 | true
7 | Disk
8 | false
9 | Foreground
10 | 7
11 | Days
12 | false
13 | false
14 | true
15 | 0
16 | 1.0.0.%2a
17 | false
18 | false
19 | true
20 | false
21 | true
22 |
23 |
24 | app.manifest
25 |
26 |
27 |
28 | UserControl
29 |
30 |
31 |
32 |
33 | False
34 | Microsoft .NET Framework 4.7.2 %28x86 und x64%29
35 | true
36 |
37 |
38 | False
39 | .NET Framework 3.5 SP1
40 | false
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | all
49 | runtime; build; native; contentfiles; analyzers; buildtransitive
50 |
51 |
52 |
53 | all
54 | runtime; build; native; contentfiles; analyzers; buildtransitive
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/PolygonDisplay/PolygonDisplay.csproj.user:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | publish\
5 |
6 |
7 |
8 |
9 |
10 | de-DE
11 | false
12 |
13 |
14 |
15 | Form
16 |
17 |
18 |
--------------------------------------------------------------------------------
/PolygonDisplay/PolygonDrawControl.Designer.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonDisplay
2 | {
3 | partial class PolygonDrawControl
4 | {
5 | ///
6 | /// Erforderliche Designervariable.
7 | ///
8 | private System.ComponentModel.IContainer components = null;
9 |
10 | ///
11 | /// Verwendete Ressourcen bereinigen.
12 | ///
13 | /// True, wenn verwaltete Ressourcen gelöscht werden sollen; andernfalls False.
14 | protected override void Dispose(bool disposing)
15 | {
16 | if (disposing && (components != null))
17 | {
18 | components.Dispose();
19 | }
20 | base.Dispose(disposing);
21 | }
22 |
23 | #region Vom Komponenten-Designer generierter Code
24 |
25 | ///
26 | /// Erforderliche Methode für die Designerunterstützung.
27 | /// Der Inhalt der Methode darf nicht mit dem Code-Editor geändert werden.
28 | ///
29 | private void InitializeComponent()
30 | {
31 | this.SuspendLayout();
32 | //
33 | // PolygonDrawControl
34 | //
35 | this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F);
36 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
37 | this.BackColor = System.Drawing.Color.White;
38 | this.Name = "PolygonDrawControl";
39 | this.Size = new System.Drawing.Size(716, 551);
40 | this.ResumeLayout(false);
41 |
42 | }
43 |
44 | #endregion
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/PolygonDisplay/PolygonDrawControl.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
--------------------------------------------------------------------------------
/PolygonDisplay/Program.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonDisplay
2 | {
3 | using System;
4 | using System.Windows.Forms;
5 |
6 | ///
7 | /// the main program
8 | ///
9 | public static class Program
10 | {
11 | ///
12 | /// Der Haupteinstiegspunkt für die Anwendung.
13 | ///
14 | [STAThread]
15 | public static void Main()
16 | {
17 | Application.EnableVisualStyles();
18 | Application.SetCompatibleTextRenderingDefault(false);
19 | Application.Run(new Form1());
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/PolygonDisplay/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.InteropServices;
3 |
4 | // Allgemeine Informationen über eine Assembly werden über die folgenden
5 | // Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern,
6 | // die einer Assembly zugeordnet sind.
7 | [assembly: AssemblyTitle("PolygonDisplay")]
8 | [assembly: AssemblyDescription("")]
9 | [assembly: AssemblyConfiguration("")]
10 | [assembly: AssemblyCompany("")]
11 | [assembly: AssemblyProduct("PolygonDisplay")]
12 | [assembly: AssemblyCopyright("Copyright © 2020")]
13 | [assembly: AssemblyTrademark("")]
14 | [assembly: AssemblyCulture("")]
15 |
16 | // Durch Festlegen von ComVisible auf FALSE werden die Typen in dieser Assembly
17 | // für COM-Komponenten unsichtbar. Wenn Sie auf einen Typ in dieser Assembly von
18 | // COM aus zugreifen müssen, sollten Sie das ComVisible-Attribut für diesen Typ auf "True" festlegen.
19 | [assembly: ComVisible(false)]
20 |
21 | // Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird
22 | [assembly: Guid("8898db4e-4452-4a83-b4f5-b518bbab5eb6")]
23 |
24 | // Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten:
25 | //
26 | // Hauptversion
27 | // Nebenversion
28 | // Buildnummer
29 | // Revision
30 | //
31 | // Sie können alle Werte angeben oder Standardwerte für die Build- und Revisionsnummern verwenden,
32 | // indem Sie "*" wie unten gezeigt eingeben:
33 | // [assembly: AssemblyVersion("1.0.*")]
34 | [assembly: AssemblyVersion("1.0.0.0")]
35 | [assembly: AssemblyFileVersion("1.0.0.0")]
36 |
--------------------------------------------------------------------------------
/PolygonDisplay/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // Dieser Code wurde von einem Tool generiert.
4 | // Laufzeitversion: 4.0.30319.42000
5 | //
6 | // Änderungen an dieser Datei können fehlerhaftes Verhalten verursachen und gehen verloren, wenn
7 | // der Code neu generiert wird.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace PolygonDisplay.Properties
12 | {
13 |
14 |
15 | ///
16 | /// Eine stark typisierte Ressourcenklasse zum Suchen von lokalisierten Zeichenfolgen usw.
17 | ///
18 | // Diese Klasse wurde von der StronglyTypedResourceBuilder-Klasse
19 | // über ein Tool wie ResGen oder Visual Studio automatisch generiert.
20 | // Um einen Member hinzuzufügen oder zu entfernen, bearbeiten Sie die .ResX-Datei und führen dann ResGen
21 | // mit der Option /str erneut aus, oder erstellen Sie Ihr VS-Projekt neu.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources
26 | {
27 |
28 | private static global::System.Resources.ResourceManager resourceMan;
29 |
30 | private static global::System.Globalization.CultureInfo resourceCulture;
31 |
32 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
33 | internal Resources()
34 | {
35 | }
36 |
37 | ///
38 | /// Gibt die zwischengespeicherte ResourceManager-Instanz zurück, die von dieser Klasse verwendet wird.
39 | ///
40 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
41 | internal static global::System.Resources.ResourceManager ResourceManager
42 | {
43 | get
44 | {
45 | if ((resourceMan == null))
46 | {
47 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PolygonDisplay.Properties.Resources", typeof(Resources).Assembly);
48 | resourceMan = temp;
49 | }
50 | return resourceMan;
51 | }
52 | }
53 |
54 | ///
55 | /// Überschreibt die CurrentUICulture-Eigenschaft des aktuellen Threads für alle
56 | /// Ressourcenlookups, die diese stark typisierte Ressourcenklasse verwenden.
57 | ///
58 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
59 | internal static global::System.Globalization.CultureInfo Culture
60 | {
61 | get
62 | {
63 | return resourceCulture;
64 | }
65 | set
66 | {
67 | resourceCulture = value;
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/PolygonDisplay/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | text/microsoft-resx
107 |
108 |
109 | 2.0
110 |
111 |
112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
113 |
114 |
115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
--------------------------------------------------------------------------------
/PolygonDisplay/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace PolygonDisplay.Properties
12 | {
13 |
14 |
15 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
16 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
17 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
18 | {
19 |
20 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
21 |
22 | public static Settings Default
23 | {
24 | get
25 | {
26 | return defaultInstance;
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/PolygonDisplay/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/PolygonDisplay/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
52 |
59 |
60 |
61 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/PolygonTriangulation.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30002.166
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PolygonTriangulation", "PolygonTriangulation\PolygonTriangulation.csproj", "{250832D7-A812-4B2A-9EF7-F1791EC657FF}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TriangulationTests", "TriangulationTests\TriangulationTests.csproj", "{981FE2F3-4D3E-4F7F-8A64-32FD485330BB}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PolygonDisplay", "PolygonDisplay\PolygonDisplay.csproj", "{8898DB4E-4452-4A83-B4F5-B518BBAB5EB6}"
11 | EndProject
12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2EFAEA6D-3965-464E-A0AD-188667A80217}"
13 | ProjectSection(SolutionItems) = preProject
14 | .editorconfig = .editorconfig
15 | Directory.Build.props = Directory.Build.props
16 | EndProjectSection
17 | EndProject
18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Documentation", "Documentation", "{B69883E5-A4DC-4811-89E6-B0A24BFA8865}"
19 | ProjectSection(SolutionItems) = preProject
20 | Documentation\Monotones.md = Documentation\Monotones.md
21 | Documentation\Polygon.md = Documentation\Polygon.md
22 | Readme.md = Readme.md
23 | Documentation\Trapezoidation.md = Documentation\Trapezoidation.md
24 | EndProjectSection
25 | EndProject
26 | Global
27 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
28 | Debug|Any CPU = Debug|Any CPU
29 | Release|Any CPU = Release|Any CPU
30 | EndGlobalSection
31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
32 | {250832D7-A812-4B2A-9EF7-F1791EC657FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {250832D7-A812-4B2A-9EF7-F1791EC657FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {250832D7-A812-4B2A-9EF7-F1791EC657FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {250832D7-A812-4B2A-9EF7-F1791EC657FF}.Release|Any CPU.Build.0 = Release|Any CPU
36 | {981FE2F3-4D3E-4F7F-8A64-32FD485330BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
37 | {981FE2F3-4D3E-4F7F-8A64-32FD485330BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
38 | {981FE2F3-4D3E-4F7F-8A64-32FD485330BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
39 | {981FE2F3-4D3E-4F7F-8A64-32FD485330BB}.Release|Any CPU.Build.0 = Release|Any CPU
40 | {8898DB4E-4452-4A83-B4F5-B518BBAB5EB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
41 | {8898DB4E-4452-4A83-B4F5-B518BBAB5EB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
42 | {8898DB4E-4452-4A83-B4F5-B518BBAB5EB6}.Release|Any CPU.ActiveCfg = Release|Any CPU
43 | {8898DB4E-4452-4A83-B4F5-B518BBAB5EB6}.Release|Any CPU.Build.0 = Release|Any CPU
44 | EndGlobalSection
45 | GlobalSection(SolutionProperties) = preSolution
46 | HideSolutionNode = FALSE
47 | EndGlobalSection
48 | GlobalSection(NestedProjects) = preSolution
49 | {B69883E5-A4DC-4811-89E6-B0A24BFA8865} = {2EFAEA6D-3965-464E-A0AD-188667A80217}
50 | EndGlobalSection
51 | GlobalSection(ExtensibilityGlobals) = postSolution
52 | SolutionGuid = {00E046C1-DF26-4181-B779-F6383697110B}
53 | EndGlobalSection
54 | EndGlobal
55 |
--------------------------------------------------------------------------------
/PolygonTriangulation/PlanePolygonBuilder.ClusterVertexComparer.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 |
6 | #if UNITY_EDITOR || UNITY_STANDALONE
7 | using Vertex = UnityEngine.Vector2;
8 | #else
9 | using Vertex = System.Numerics.Vector2;
10 | #endif
11 |
12 | ///
13 | /// subclass container
14 | ///
15 | public partial class PlanePolygonBuilder
16 | {
17 | ///
18 | /// Compare two vertices, very close vertices are considered equal.
19 | ///
20 | private class ClusterVertexComparer : IComparer
21 | {
22 | ///
23 | public int Compare(Vertex x, Vertex y)
24 | {
25 | #if UNITY_EDITOR || UNITY_STANDALONE
26 | var xdist = Math.Abs(x.x - y.x);
27 | if (xdist < epsilon)
28 | {
29 | var ydist = Math.Abs(x.y - y.y);
30 | if (ydist < epsilon)
31 | {
32 | return 0;
33 | }
34 |
35 | var xCompare = x.x.CompareTo(y.x);
36 | if (xCompare != 0)
37 | {
38 | return xCompare;
39 | }
40 |
41 | if (x.y < y.y)
42 | {
43 | return -1;
44 | }
45 | else
46 | {
47 | return 1;
48 | }
49 | }
50 | else if (x.x < y.x)
51 | {
52 | return -1;
53 | }
54 | else
55 | {
56 | return 1;
57 | }
58 | #else
59 | var xdist = Math.Abs(x.X - y.X);
60 | if (xdist < Epsilon)
61 | {
62 | var ydist = Math.Abs(x.Y - y.Y);
63 | if (ydist < Epsilon)
64 | {
65 | return 0;
66 | }
67 |
68 | var xCompare = x.X.CompareTo(y.X);
69 | if (xCompare != 0)
70 | {
71 | return xCompare;
72 | }
73 |
74 | if (x.Y < y.Y)
75 | {
76 | return -1;
77 | }
78 | else
79 | {
80 | return 1;
81 | }
82 | }
83 | else if (x.X < y.X)
84 | {
85 | return -1;
86 | }
87 | else
88 | {
89 | return 1;
90 | }
91 | #endif
92 | }
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/PolygonTriangulation/PlanePolygonBuilder.EdgesToPolygonBuilder.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Text;
7 |
8 | #if UNITY_EDITOR || UNITY_STANDALONE
9 | using Quaternion = UnityEngine.Quaternion;
10 | using Vector3 = UnityEngine.Vector3;
11 | using Vertex = UnityEngine.Vector2;
12 | #else
13 | using Quaternion = System.Numerics.Quaternion;
14 | using Vector3 = System.Numerics.Vector3;
15 | using Vertex = System.Numerics.Vector2;
16 | #endif
17 |
18 | ///
19 | /// Test interface for the polygon builder
20 | ///
21 | internal interface IEdgesToPolygonBuilder
22 | {
23 | ///
24 | /// Add an edge between the two points
25 | ///
26 | /// start point
27 | /// end point
28 | void AddEdge(Vector3 p0, Vector3 p1);
29 |
30 | ///
31 | /// build the resulting polygon
32 | ///
33 | /// A polygon and the 3D vertices
34 | IPlanePolygon BuildPolygon();
35 | }
36 |
37 | ///
38 | /// subclass container
39 | ///
40 | public partial class PlanePolygonBuilder
41 | {
42 | ///
43 | /// Build a polygon from 3D edges, lying on a common plane.
44 | ///
45 | private class EdgesToPolygonBuilder : IEdgesToPolygonBuilder
46 | {
47 | ///
48 | /// An empty hash set
49 | ///
50 | private static readonly ICollection EmptyHashSet = new HashSet();
51 |
52 | ///
53 | /// current edges in pairs
54 | ///
55 | private readonly List edges;
56 |
57 | ///
58 | /// original vertices
59 | ///
60 | private readonly List vertices3D;
61 |
62 | ///
63 | /// rotated vertices
64 | ///
65 | private readonly List vertices2D;
66 |
67 | ///
68 | /// Initializes a new instance of the class.
69 | ///
70 | /// the rotation to map a vertex to a 2D plane
71 | public EdgesToPolygonBuilder(Quaternion rotation)
72 | {
73 | this.edges = new List();
74 | this.vertices3D = new List();
75 | this.vertices2D = new List();
76 |
77 | this.Rotation = rotation;
78 | }
79 |
80 | ///
81 | /// Gets the 3D to 2D rotation
82 | ///
83 | public Quaternion Rotation { get; }
84 |
85 | ///
86 | /// Dump the edges during debugging
87 | ///
88 | public string Dump()
89 | {
90 | #if DEBUG
91 | var sb = new StringBuilder();
92 |
93 | sb.AppendLine("var builder = PlanePolygonBuilder.CreatePolygonBuilder();");
94 | for (int i = 0; i < this.vertices2D.Count - 1; i += 2)
95 | {
96 | #if UNITY_EDITOR || UNITY_STANDALONE
97 | sb.AppendLine($"builder.AddEdge(new Vector3({this.vertices2D[i].x:0.00000000}f, {this.vertices2D[i].y:0.00000000}f, 0), new Vector3({this.vertices2D[i + 1].x:0.00000000}f, {this.vertices2D[i + 1].y:0.00000000}f, 0));");
98 | #else
99 | sb.AppendLine($"builder.AddEdge(new Vector3({this.vertices2D[i].X:0.00000000}f, {this.vertices2D[i].Y:0.00000000}f, 0), new Vector3({this.vertices2D[i + 1].X:0.00000000}f, {this.vertices2D[i + 1].Y:0.00000000}f, 0));");
100 | #endif
101 | }
102 |
103 | sb.AppendLine("builder.BuildPolygon();");
104 |
105 | return sb.ToString();
106 | #else
107 | return this.ToString();
108 | #endif
109 | }
110 |
111 | ///
112 | /// Add an edge
113 | ///
114 | /// start point
115 | /// end point
116 | public void AddEdge(Vector3 p0, Vector3 p1)
117 | {
118 | var planeTriangleOffset = this.vertices2D.Count;
119 | this.vertices3D.Add(p0);
120 | this.vertices3D.Add(p1);
121 | #if UNITY_EDITOR || UNITY_STANDALONE
122 | var p0Rotated = this.Rotation * p0;
123 | this.vertices2D.Add(new Vertex(p0Rotated.x, p0Rotated.y));
124 |
125 | var p1Rotated = this.Rotation * p1;
126 | this.vertices2D.Add(new Vertex(p1Rotated.x, p1Rotated.y));
127 | #else
128 | var p0Rotated = Vector3.Transform(p0, this.Rotation);
129 | this.vertices2D.Add(new Vertex(p0Rotated.X, p0Rotated.Y));
130 |
131 | var p1Rotated = Vector3.Transform(p1, this.Rotation);
132 | this.vertices2D.Add(new Vertex(p1Rotated.X, p1Rotated.Y));
133 | #endif
134 |
135 | this.edges.Add(planeTriangleOffset + 0);
136 | this.edges.Add(planeTriangleOffset + 1);
137 | }
138 |
139 | ///
140 | /// Compress the vertices and connect all edges to polygons
141 | ///
142 | /// the polygon and the 3D vertices
143 | public IPlanePolygon BuildPolygon()
144 | {
145 | var sorted2D = this.vertices2D.ToArray();
146 | var sortedIndizes = Enumerable.Range(0, sorted2D.Length).ToArray();
147 | var comparer = new ClusterVertexComparer();
148 | ICollection fusionedVertices = null;
149 | Array.Sort(sorted2D, sortedIndizes, comparer);
150 |
151 | var translation = new int[sorted2D.Length];
152 | var writeIndex = 0;
153 | var sameVertexCount = 0;
154 | for (int i = 1; i < sorted2D.Length; i++)
155 | {
156 | if (comparer.Compare(sorted2D[writeIndex], sorted2D[i]) != 0)
157 | {
158 | sorted2D[++writeIndex] = sorted2D[i];
159 | sameVertexCount = 0;
160 | }
161 | else
162 | {
163 | if (++sameVertexCount == 2)
164 | {
165 | fusionedVertices = fusionedVertices ?? new HashSet();
166 | fusionedVertices.Add(writeIndex);
167 | }
168 | }
169 |
170 | translation[sortedIndizes[i]] = writeIndex;
171 | }
172 |
173 | var count = writeIndex + 1;
174 | Array.Resize(ref sorted2D, count);
175 |
176 | var compressed3D = new Vector3[sorted2D.Length];
177 | for (int i = 0; i < translation.Length; i++)
178 | {
179 | compressed3D[translation[i]] = this.vertices3D[i];
180 | }
181 |
182 | var lineDetector = new PolygonLineDetector(fusionedVertices ?? EmptyHashSet);
183 | lineDetector.JoinEdgesToPolygones(this.edges.Select(x => translation[x]));
184 |
185 | if (lineDetector.UnclosedPolygons.Any())
186 | {
187 | lineDetector.TryClusteringUnclosedEnds(sorted2D, Epsilon * 100);
188 | }
189 |
190 | var polygon = Polygon.FromPolygonLines(sorted2D, lineDetector.Lines.Select(x => x.ToIndexes()).ToArray(), fusionedVertices);
191 | return new PlanePolygonData(compressed3D, polygon);
192 | }
193 | }
194 | }
195 | }
--------------------------------------------------------------------------------
/PolygonTriangulation/PlanePolygonBuilder.PlanePolygonData.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | #if UNITY_EDITOR || UNITY_STANDALONE
4 | using Vector3 = UnityEngine.Vector3;
5 | #else
6 | using Vector3 = System.Numerics.Vector3;
7 | #endif
8 |
9 | ///
10 | /// The polygon result after combining all edges
11 | ///
12 | public interface IPlanePolygon
13 | {
14 | ///
15 | /// Gets the 3D vertices.
16 | ///
17 | Vector3[] Vertices { get; }
18 |
19 | ///
20 | /// Gets the polygon. It contains the 2D vertices.
21 | ///
22 | Polygon Polygon { get; }
23 | }
24 |
25 | ///
26 | /// subclass container
27 | ///
28 | public partial class PlanePolygonBuilder
29 | {
30 | ///
31 | /// Result storage for polygon data
32 | ///
33 | private class PlanePolygonData : IPlanePolygon
34 | {
35 | public PlanePolygonData(Vector3[] vertices3D, Polygon polygon)
36 | {
37 | this.Vertices = vertices3D;
38 | this.Polygon = polygon;
39 | }
40 |
41 | ///
42 | public Vector3[] Vertices { get; }
43 |
44 | ///
45 | public Polygon Polygon { get; }
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/PolygonTriangulation/PlanePolygonBuilder.PolygonLineDetector.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 |
7 | #if UNITY_EDITOR || UNITY_STANDALONE
8 | using Vertex = UnityEngine.Vector2;
9 | #else
10 | using Vertex = System.Numerics.Vector2;
11 | #endif
12 |
13 | ///
14 | /// Test interface for the polygon line detector (join edges to polygon lines)
15 | ///
16 | public interface IPolygonLineDetector
17 | {
18 | ///
19 | /// Gets the closed polygons
20 | ///
21 | IEnumerable> ClosedPolygons { get; }
22 |
23 | ///
24 | /// Gets the unclosed polygons
25 | ///
26 | IEnumerable> UnclosedPolygons { get; }
27 |
28 | ///
29 | /// Add multiple edges
30 | ///
31 | /// pairs of vertex ids
32 | void JoinEdgesToPolygones(IEnumerable edges);
33 |
34 | ///
35 | /// Try to close unclosed polygons by connecting close vertices.
36 | ///
37 | /// the vertices
38 | /// the maximum distance between vertices
39 | void TryClusteringUnclosedEnds(Vertex[] vertices, float maxDistance);
40 | }
41 |
42 | ///
43 | /// subclass container
44 | ///
45 | public partial class PlanePolygonBuilder
46 | {
47 | ///
48 | /// Detect closed polygon lines
49 | ///
50 | private class PolygonLineDetector : IPolygonLineDetector
51 | {
52 | ///
53 | /// Mapping from start/end of polygon line to the line instance.
54 | ///
55 | private readonly Dictionary openPolygones;
56 |
57 | ///
58 | /// Closed polygon lines
59 | ///
60 | private readonly List closedPolygones;
61 |
62 | ///
63 | /// Unclosed polygon lines
64 | ///
65 | private readonly List unclosedPolygones;
66 |
67 | ///
68 | /// The fusion vertices
69 | ///
70 | private readonly ICollection fusionVertices;
71 |
72 | ///
73 | /// Segments with fusion point that need delay
74 | ///
75 | private readonly List<(int, int)> fusionDelayedSegments;
76 |
77 | ///
78 | /// Initializes a new instance of the class.
79 | ///
80 | /// Vertices that are used by more than two edges
81 | public PolygonLineDetector(ICollection fusionVertices)
82 | {
83 | this.openPolygones = new Dictionary();
84 | this.closedPolygones = new List();
85 | this.unclosedPolygones = new List();
86 | this.fusionVertices = fusionVertices;
87 | this.fusionDelayedSegments = fusionVertices.Any() ? new List<(int, int)>() : null;
88 | }
89 |
90 | ///
91 | /// Gets the closed polygones.
92 | ///
93 | public IReadOnlyList Lines => this.closedPolygones;
94 |
95 | ///
96 | public IEnumerable> ClosedPolygons => this.closedPolygones.Select(x => x.ToIndexes());
97 |
98 | ///
99 | public IEnumerable> UnclosedPolygons => this.unclosedPolygones.Select(x => x.ToIndexes());
100 |
101 | ///
102 | /// find continous combination of edges
103 | ///
104 | /// triangle data, the relevant edges are from vertex0 to vertex1. vertex2 is ignored
105 | public void JoinEdgesToPolygones(IEnumerable edges)
106 | {
107 | var iterator = edges.GetEnumerator();
108 | while (iterator.MoveNext())
109 | {
110 | var start = iterator.Current;
111 | iterator.MoveNext();
112 | var end = iterator.Current;
113 |
114 | if (start == end)
115 | {
116 | continue;
117 | }
118 |
119 | this.AddEdge(start, end);
120 | }
121 |
122 | if (this.fusionDelayedSegments?.Count > 0)
123 | {
124 | foreach (var (start, end) in this.fusionDelayedSegments)
125 | {
126 | this.AddEdge(start, end);
127 | }
128 | }
129 |
130 | this.unclosedPolygones.AddRange(this.openPolygones
131 | .Where(x => x.Key == x.Value.StartKey)
132 | .Select(x => x.Value));
133 | }
134 |
135 | ///
136 | public void TryClusteringUnclosedEnds(Vertex[] vertices, float maxDistance)
137 | {
138 | bool vertexFound;
139 | do
140 | {
141 | vertexFound = false;
142 | foreach (var vertexId in this.openPolygones.Keys)
143 | {
144 | var closestPeer = this.openPolygones.Keys
145 | .Where(x => x != vertexId)
146 | .OrderBy(x => Distance(vertices, vertexId, x))
147 | .First();
148 | if (Distance(vertices, vertexId, closestPeer) < maxDistance)
149 | {
150 | this.JoinClusteredVertex(vertexId, closestPeer);
151 | vertexFound = true;
152 | break;
153 | }
154 | }
155 | }
156 | while (vertexFound);
157 |
158 | this.unclosedPolygones.Clear();
159 | this.unclosedPolygones.AddRange(this.openPolygones
160 | .Where(x => x.Key == x.Value.StartKey)
161 | .Select(x => x.Value));
162 | }
163 |
164 | ///
165 | /// Calculate the distance between two points
166 | ///
167 | /// the vertices
168 | /// the first point
169 | /// the second point
170 | /// the sum of the x and y distance
171 | private static float Distance(Vertex[] vertices, int vertexId, int peer)
172 | {
173 | #if UNITY_EDITOR || UNITY_STANDALONE
174 | return Math.Abs(vertices[vertexId].x - vertices[peer].x) + Math.Abs(vertices[vertexId].y - vertices[peer].y);
175 | #else
176 | return Math.Abs(vertices[vertexId].X - vertices[peer].X) + Math.Abs(vertices[vertexId].Y - vertices[peer].Y);
177 | #endif
178 | }
179 |
180 | ///
181 | /// Join two vertices as they are close together.
182 | ///
183 | /// the vertex id to drop
184 | /// the closest existing peer, that will act as replacement
185 | private void JoinClusteredVertex(int vertexId, int closestPeer)
186 | {
187 | var vertexSegment = this.openPolygones[vertexId];
188 | this.openPolygones.Remove(vertexId);
189 | var peerIsLineStart = vertexSegment.RemoveVertex(vertexId);
190 |
191 | var peerSegment = this.openPolygones[closestPeer];
192 | this.openPolygones.Remove(closestPeer);
193 |
194 | var vertexReplacement = peerIsLineStart ? vertexSegment.StartKey : vertexSegment.EndKey;
195 | var joinedKey = peerSegment.Join(vertexSegment, vertexReplacement, closestPeer);
196 | if (joinedKey < 0)
197 | {
198 | this.closedPolygones.Add(vertexSegment);
199 | }
200 | else
201 | {
202 | this.openPolygones.Remove(peerIsLineStart ? vertexSegment.EndKey : vertexSegment.StartKey);
203 | this.openPolygones.Add(joinedKey, peerSegment);
204 | }
205 | }
206 |
207 | ///
208 | /// Add a new edge to the polygon line. Either join two polygon lines, creates a new or adds the edge to the neighboring line
209 | ///
210 | /// the vertex id of the edge start
211 | /// the vertex id of the edge end
212 | private void AddEdge(int start, int end)
213 | {
214 | var startFits = this.openPolygones.TryGetValue(start, out var firstSegment);
215 | if (startFits)
216 | {
217 | this.openPolygones.Remove(start);
218 | }
219 |
220 | var endFits = this.openPolygones.TryGetValue(end, out var lastSegment);
221 | if (endFits)
222 | {
223 | this.openPolygones.Remove(end);
224 | }
225 |
226 | if (!startFits && !endFits)
227 | {
228 | var segment = new PolygonLine(start, end);
229 | this.openPolygones.Add(start, segment);
230 | this.openPolygones.Add(end, segment);
231 | }
232 | else if (startFits && endFits)
233 | {
234 | var remainingKeyOfOther = firstSegment.Join(lastSegment, start, end);
235 | if (remainingKeyOfOther < 0)
236 | {
237 | this.closedPolygones.Add(firstSegment);
238 | }
239 | else
240 | {
241 | this.openPolygones[remainingKeyOfOther] = firstSegment;
242 | }
243 | }
244 | else if (startFits)
245 | {
246 | if ((start == firstSegment.EndKey) || !this.fusionVertices.Contains(start))
247 | {
248 | firstSegment.AddMatchingStart(start, end);
249 | this.openPolygones[end] = firstSegment;
250 | }
251 | else
252 | {
253 | this.fusionDelayedSegments.Add((start, end));
254 | this.openPolygones[start] = firstSegment;
255 | }
256 | }
257 | else
258 | {
259 | if ((end == lastSegment.StartKey) || !this.fusionVertices.Contains(end))
260 | {
261 | lastSegment.AddMatchingEnd(start, end);
262 | this.openPolygones[start] = lastSegment;
263 | }
264 | else
265 | {
266 | this.fusionDelayedSegments.Add((start, end));
267 | this.openPolygones[end] = lastSegment;
268 | }
269 | }
270 | }
271 | }
272 | }
273 | }
--------------------------------------------------------------------------------
/PolygonTriangulation/PlanePolygonBuilder.TriangulatedPlanePolygon.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System.Collections.Generic;
4 |
5 | #if UNITY_EDITOR || UNITY_STANDALONE
6 | using Vector3 = UnityEngine.Vector3;
7 | using Vertex = UnityEngine.Vector2;
8 | #else
9 | using Vector3 = System.Numerics.Vector3;
10 | using Vertex = System.Numerics.Vector2;
11 | #endif
12 |
13 | ///
14 | /// Resulting plane mesh with coordinates and triangles
15 | ///
16 | public interface ITriangulatedPlanePolygon
17 | {
18 | ///
19 | /// Gets the 3D vertices
20 | ///
21 | Vector3[] Vertices { get; }
22 |
23 | ///
24 | /// Gets the 2D vertices of the plane points
25 | ///
26 | IReadOnlyList Vertices2D { get; }
27 |
28 | ///
29 | /// Gets the triangles with vertex offset
30 | ///
31 | int[] Triangles { get; }
32 | }
33 |
34 | ///
35 | /// subclass container
36 | ///
37 | public partial class PlanePolygonBuilder
38 | {
39 | ///
40 | /// Result for the plane mesh
41 | ///
42 | private class TriangulatedPlanePolygon : ITriangulatedPlanePolygon
43 | {
44 | public TriangulatedPlanePolygon(Vector3[] vertices, IReadOnlyList vertices2D, int[] triangles)
45 | {
46 | this.Vertices = vertices;
47 | this.Triangles = triangles;
48 | this.Vertices2D = vertices2D;
49 | }
50 |
51 | ///
52 | public Vector3[] Vertices { get; }
53 |
54 | ///
55 | public int[] Triangles { get; }
56 |
57 | ///
58 | public IReadOnlyList Vertices2D { get; }
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/PolygonTriangulation/PlanePolygonBuilder.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System;
4 |
5 | #if UNITY_EDITOR || UNITY_STANDALONE
6 | using Plane = UnityEngine.Plane;
7 | using Quaternion = UnityEngine.Quaternion;
8 | using Vector3 = UnityEngine.Vector3;
9 | #else
10 | using Plane = System.Numerics.Plane;
11 | using Quaternion = System.Numerics.Quaternion;
12 | using Vector3 = System.Numerics.Vector3;
13 | #endif
14 |
15 | ///
16 | /// Collect the edges for a plane polygon
17 | ///
18 | public interface IPlanePolygonEdgeCollector
19 | {
20 | ///
21 | /// Add an edge
22 | ///
23 | /// start point
24 | /// end point
25 | void AddEdge(Vector3 p0, Vector3 p1);
26 |
27 | ///
28 | /// Dump the collected edges
29 | ///
30 | /// dump the collected edges for debug
31 | string Dump();
32 | }
33 |
34 | ///
35 | /// Build a list of triangles from polygon edges
36 | ///
37 | public partial class PlanePolygonBuilder : IPlanePolygonEdgeCollector
38 | {
39 | private const float Epsilon = 1.1E-5f;
40 |
41 | private readonly EdgesToPolygonBuilder edgesToPolygon;
42 |
43 | ///
44 | /// Initializes a new instance of the class.
45 | ///
46 | /// The plane to rotate the 3D point into 2D.
47 | public PlanePolygonBuilder(Plane plane)
48 | {
49 | #if UNITY_EDITOR || UNITY_STANDALONE
50 | var rotation = Quaternion.FromToRotation(plane.normal, new Vector3(0, 0, -1));
51 | #else
52 | var rotation = IdendityQuaternion;
53 | if (plane.Normal != Vector3.UnitZ)
54 | {
55 | throw new NotImplementedException("rotation setup is not implemented");
56 | }
57 | #endif
58 | this.edgesToPolygon = new EdgesToPolygonBuilder(rotation);
59 | }
60 |
61 | ///
62 | /// Gets the 3D to 2D rotation
63 | ///
64 | public Quaternion Rotation => this.edgesToPolygon.Rotation;
65 |
66 | #if UNITY_EDITOR || UNITY_STANDALONE
67 | private static Quaternion IdendityQuaternion => Quaternion.identity;
68 | #else
69 | private static Quaternion IdendityQuaternion => Quaternion.Identity;
70 | #endif
71 |
72 | ///
73 | public void AddEdge(Vector3 p0, Vector3 p1)
74 | {
75 | this.edgesToPolygon.AddEdge(p0, p1);
76 | }
77 |
78 | ///
79 | public string Dump()
80 | {
81 | return this.edgesToPolygon.Dump();
82 | }
83 |
84 | ///
85 | /// Build the plane triangles
86 | ///
87 | /// The triangles, the 2D vertices and the 3D vertices
88 | public ITriangulatedPlanePolygon Build()
89 | {
90 | IPlanePolygon polygonResult = null;
91 | try
92 | {
93 | polygonResult = this.edgesToPolygon.BuildPolygon();
94 | var triangulator = new PolygonTriangulator(polygonResult.Polygon);
95 | var triangles = triangulator.BuildTriangles();
96 | return new TriangulatedPlanePolygon(polygonResult.Vertices, polygonResult.Polygon.Vertices, triangles);
97 | }
98 | catch (Exception e)
99 | {
100 | throw new TriangulationException(polygonResult?.Polygon, this.edgesToPolygon.Dump(), e);
101 | }
102 | }
103 |
104 | ///
105 | /// Create a edges to polygon builder for unit testing
106 | ///
107 | /// the polygon builder
108 | internal static IEdgesToPolygonBuilder CreatePolygonBuilder() => new EdgesToPolygonBuilder(IdendityQuaternion);
109 |
110 | ///
111 | /// Create a polygon from edges detector. The edge is defined by the vertex ids
112 | ///
113 | /// The fusion vertices.
114 | /// the detector
115 | internal static IPolygonLineDetector CreatePolygonLineDetector(params int[] fusionVertices) => new PolygonLineDetector(fusionVertices);
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/PolygonTriangulation/Polygon.Builder.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System.Collections.Generic;
4 | using System.Linq;
5 |
6 | #if UNITY_EDITOR || UNITY_STANDALONE
7 | using Vertex = UnityEngine.Vector2;
8 | #else
9 | using Vertex = System.Numerics.Vector2;
10 | #endif
11 |
12 | ///
13 | /// Build a polygon
14 | ///
15 | public interface IPolygonBuilder
16 | {
17 | ///
18 | /// Add a single vertex id
19 | ///
20 | /// the index in the vertices array of the builder
21 | /// the same builder instance for call chains
22 | IPolygonBuilder Add(int vertexId);
23 |
24 | ///
25 | /// Add multiple vertex ids
26 | ///
27 | /// the indices in the vertices array of the builder
28 | /// the same builder instance for call chains
29 | IPolygonBuilder AddVertices(IEnumerable vertices);
30 |
31 | ///
32 | /// Close the current polygon. Next vertices are considered a new polygon line i.e a hole or a non-intersecting polygon
33 | ///
34 | /// the same builder instance for call chains
35 | IPolygonBuilder ClosePartialPolygon();
36 |
37 | ///
38 | /// Close the polygon. Do not use the builder after closing it.
39 | ///
40 | /// vertices that are used in more than one subpolygon
41 | /// a polygon
42 | Polygon Close(params int[] fusionVertices);
43 |
44 | ///
45 | /// Create one polygon that includes all vertices in the builder.
46 | ///
47 | /// a polygon
48 | Polygon Auto();
49 | }
50 |
51 | ///
52 | /// subclass container for polygon
53 | ///
54 | public partial class Polygon
55 | {
56 | ///
57 | /// Build a new polygon
58 | ///
59 | private class PolygonBuilder : IPolygonBuilder
60 | {
61 | private readonly Vertex[] vertices;
62 | private readonly List vertexIds;
63 | private readonly List nextIndices;
64 | private readonly List polygonIds;
65 | private int first;
66 | private int polygonId;
67 |
68 | public PolygonBuilder(Vertex[] vertices)
69 | {
70 | this.first = 0;
71 | this.vertices = vertices;
72 | this.vertexIds = new List();
73 | this.nextIndices = new List();
74 | this.polygonIds = new List();
75 | this.polygonId = 0;
76 | }
77 |
78 | public Polygon Auto()
79 | {
80 | return Polygon.FromVertexList(
81 | this.vertices,
82 | Enumerable.Range(0, this.vertices.Length),
83 | Enumerable.Range(1, this.vertices.Length - 1).Concat(Enumerable.Range(0, 1)),
84 | Enumerable.Repeat(0, this.vertices.Length),
85 | null);
86 | }
87 |
88 | public IPolygonBuilder Add(int vertexId)
89 | {
90 | this.nextIndices.Add(this.nextIndices.Count + 1);
91 | this.vertexIds.Add(vertexId);
92 | this.polygonIds.Add(this.polygonId);
93 | return this;
94 | }
95 |
96 | public IPolygonBuilder AddVertices(IEnumerable vertices)
97 | {
98 | foreach (var vertex in vertices)
99 | {
100 | this.nextIndices.Add(this.nextIndices.Count + 1);
101 | this.vertexIds.Add(vertex);
102 | this.polygonIds.Add(this.polygonId);
103 | }
104 |
105 | return this;
106 | }
107 |
108 | public IPolygonBuilder ClosePartialPolygon()
109 | {
110 | if (this.vertexIds.Count > this.first)
111 | {
112 | this.nextIndices[this.nextIndices.Count - 1] = this.first;
113 | this.polygonId++;
114 | this.first = this.vertexIds.Count;
115 | }
116 |
117 | return this;
118 | }
119 |
120 | public Polygon Close(params int[] fusionVertices)
121 | {
122 | this.ClosePartialPolygon();
123 | return Polygon.FromVertexList(this.vertices, this.vertexIds, this.nextIndices, this.polygonIds, fusionVertices);
124 | }
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/PolygonTriangulation/Polygon.Extensions.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System.Collections.Generic;
4 |
5 | #if UNITY_EDITOR || UNITY_STANDALONE
6 | using Vertex = UnityEngine.Vector2;
7 | #else
8 | using Vertex = System.Numerics.Vector2;
9 |
10 | #endif
11 |
12 | ///
13 | /// Extension methods for polygon
14 | ///
15 | public static class PolygonExtensions
16 | {
17 | ///
18 | /// Adds the vertices.
19 | ///
20 | /// The builder.
21 | /// The vertices.
22 | /// the same builder
23 | public static IPolygonBuilder AddVertices(this IPolygonBuilder builder, params int[] vertices)
24 | {
25 | return builder.AddVertices((IEnumerable)vertices);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/PolygonTriangulation/Polygon.NextChainEnumerable.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System;
4 | using System.Collections;
5 | using System.Collections.Generic;
6 |
7 | ///
8 | /// subclass container for polygon
9 | ///
10 | public partial class Polygon
11 | {
12 | ///
13 | /// An enumerable that creates a
14 | ///
15 | private class NextChainEnumerable : IEnumerable
16 | {
17 | private readonly int start;
18 | private readonly IReadOnlyList chain;
19 |
20 | ///
21 | /// Initializes a new instance of the class.
22 | ///
23 | /// The start.
24 | /// The chain.
25 | public NextChainEnumerable(int start, IReadOnlyList chain)
26 | {
27 | this.start = start;
28 | this.chain = chain;
29 | }
30 |
31 | ///
32 | public IEnumerator GetEnumerator() => new NextChainEnumerator(this.start, this.chain);
33 |
34 | ///
35 | IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
36 |
37 | ///
38 | /// Internal enumerator
39 | ///
40 | private sealed class NextChainEnumerator : IEnumerator
41 | {
42 | private readonly int start;
43 | private readonly IReadOnlyList chain;
44 | private bool reset;
45 | #if DEBUG
46 | private int maxIteratorCount;
47 | #endif
48 |
49 | ///
50 | /// Initializes a new instance of the class.
51 | ///
52 | /// The start.
53 | /// The chain.
54 | public NextChainEnumerator(int start, IReadOnlyList chain)
55 | {
56 | this.start = start;
57 | this.chain = chain;
58 | this.reset = true;
59 | #if DEBUG
60 | this.maxIteratorCount = chain.Count;
61 | #endif
62 | }
63 |
64 | ///
65 | public int Current { get; private set; }
66 |
67 | ///
68 | object IEnumerator.Current => this.Current;
69 |
70 | ///
71 | public void Dispose()
72 | {
73 | this.Current = -1;
74 | }
75 |
76 | ///
77 | public bool MoveNext()
78 | {
79 | if (this.reset)
80 | {
81 | this.reset = false;
82 | this.Current = this.start;
83 | }
84 | else
85 | {
86 | this.Current = this.chain[this.Current].Next;
87 | if (this.Current == this.start)
88 | {
89 | return false;
90 | }
91 | #if DEBUG
92 | if (--this.maxIteratorCount < 0)
93 | {
94 | throw new InvalidOperationException("Chain is damaged");
95 | }
96 | #endif
97 | }
98 |
99 | return true;
100 | }
101 |
102 | ///
103 | public void Reset()
104 | {
105 | this.reset = true;
106 | }
107 | }
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/PolygonTriangulation/Polygon.VertexChain.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | ///
4 | /// subclass container for polygon
5 | ///
6 | public partial class Polygon
7 | {
8 | ///
9 | /// - each chain element belongs to exactly one polygon.
10 | /// - multiple polygons are stored in the chain. (avoids copy during split)
11 | /// - if a vertex belongs to multple polygons, it has multiple chain elements with the same VertexId
12 | /// the start of that chain is in the , the collision list is in SameVertexChain
13 | /// the combination of PolygonId/VertexId is distinct.
14 | /// during polygon triangulation, the maximum collision count is 3
15 | /// - a polygon has a specific chain element as start index
16 | /// - a polygon with holes has multiple chain start elements. They are joined via
17 | ///
18 | private struct VertexChain
19 | {
20 | ///
21 | /// the index in
22 | ///
23 | public int VertexId;
24 |
25 | ///
26 | /// The id of the polygon. Holes are a separate polygon.
27 | ///
28 | public int SubPolygonId;
29 |
30 | ///
31 | /// The next info with the same vertex id.
32 | ///
33 | public int SameVertexChain;
34 |
35 | ///
36 | /// Gets the previous vertex id (not chain index)
37 | ///
38 | public int Prev { get; private set; }
39 |
40 | ///
41 | /// Gets the next chain index in the polygon (same polygon id)
42 | ///
43 | public int Next { get; private set; }
44 |
45 | ///
46 | /// Chain two items
47 | ///
48 | /// the id of the current item
49 | /// the id of the next item
50 | /// the data of the next item
51 | public void SetNext(int current, int nextChain, ref VertexChain nextItem)
52 | {
53 | this.Next = nextChain;
54 | nextItem.Prev = current;
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/PolygonTriangulation/Polygon.VertexInfo.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System.Diagnostics;
4 |
5 | ///
6 | /// The action necessary for the vertex transition.
7 | /// Ordering is important, because for the same vertex id, we need to process closing before transition before opening
8 | ///
9 | public enum VertexAction
10 | {
11 | ///
12 | /// Prev and next are left of the vertex. => This is a closing cusp.
13 | ///
14 | ClosingCusp,
15 |
16 | ///
17 | /// Transition from one vertex to the net. No cusp.
18 | ///
19 | Transition,
20 |
21 | ///
22 | /// Prev and next are right of the vertex. => This is an opening cusp.
23 | ///
24 | OpeningCusp,
25 | }
26 |
27 | ///
28 | /// Information about an element in the vertex chain of a polygon.
29 | ///
30 | public interface IPolygonVertexInfo
31 | {
32 | ///
33 | /// Gets the action necessary to process the triple
34 | ///
35 | VertexAction Action { get; }
36 |
37 | ///
38 | /// Gets the id of the current vertex
39 | ///
40 | int Id { get; }
41 |
42 | ///
43 | /// Gets the id of the next vertex
44 | ///
45 | int NextVertexId { get; }
46 |
47 | ///
48 | /// Gets the id of the previous vertex
49 | ///
50 | int PrevVertexId { get; }
51 |
52 | ///
53 | /// Gets a unique identifier for overlaying vertexes
54 | ///
55 | int Unique { get; }
56 |
57 | ///
58 | /// Gets the for the next vertex
59 | ///
60 | int NextUnique { get; }
61 |
62 | ///
63 | /// Gets the for the prev vertex
64 | ///
65 | int PrevUnique { get; }
66 | }
67 |
68 | ///
69 | /// subclass container for polygon
70 | ///
71 | public partial class Polygon
72 | {
73 | ///
74 | /// Information about an element in the vertex chain.
75 | ///
76 | [DebuggerDisplay("{Prev}>{Id}>{Next}")]
77 | private class VertexInfo : IPolygonVertexInfo
78 | {
79 | private readonly int element;
80 | private readonly VertexChain[] chain;
81 |
82 | public VertexInfo(int element, VertexChain[] chain)
83 | {
84 | this.element = element;
85 | this.chain = chain;
86 |
87 | var id = this.Id;
88 | var prev = this.PrevVertexId;
89 | var next = this.NextVertexId;
90 | if (prev < id && next < id)
91 | {
92 | this.Action = VertexAction.ClosingCusp;
93 | }
94 | else if (prev > id && next > id)
95 | {
96 | this.Action = VertexAction.OpeningCusp;
97 | }
98 | else
99 | {
100 | this.Action = VertexAction.Transition;
101 | }
102 | }
103 |
104 | ///
105 | public VertexAction Action { get; }
106 |
107 | ///
108 | public int Id => this.chain[this.element].VertexId;
109 |
110 | ///
111 | public int NextVertexId => this.chain[this.chain[this.element].Next].VertexId;
112 |
113 | ///
114 | public int PrevVertexId => this.chain[this.chain[this.element].Prev].VertexId;
115 |
116 | ///
117 | public int Unique => this.element;
118 |
119 | ///
120 | public int NextUnique => this.chain[this.element].Next;
121 |
122 | ///
123 | public int PrevUnique => this.chain[this.element].Prev;
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/PolygonTriangulation/PolygonTriangulation.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netcoreapp3.1
4 | Library
5 | false
6 |
7 |
8 | DEBUG;TRACE
9 |
10 |
11 |
12 | all
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 |
15 |
16 |
17 | all
18 | runtime; build; native; contentfiles; analyzers; buildtransitive
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/PolygonTriangulation/PolygonTriangulator.MonotonePolygonTriangulator.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | #if UNITY_EDITOR || UNITY_STANDALONE
7 | using Vertex = UnityEngine.Vector2;
8 | #else
9 | using Vertex = System.Numerics.Vector2;
10 | #endif
11 |
12 | ///
13 | /// subclass container for triangulator
14 | ///
15 | public partial class PolygonTriangulator
16 | {
17 | ///
18 | /// Class to triangluate a monotone polygon
19 | ///
20 | private class MonotonePolygonTriangulator
21 | {
22 | private readonly Polygon polygon;
23 | private readonly int subPolygonId;
24 | private readonly IReadOnlyList vertices;
25 | private readonly Stack vertexStack;
26 | private int third;
27 | private int second;
28 | private int current;
29 | private IEnumerator iterator;
30 |
31 | public MonotonePolygonTriangulator(Polygon polygon, int subPolygonId)
32 | {
33 | this.polygon = polygon;
34 | this.subPolygonId = subPolygonId;
35 | this.vertices = polygon.Vertices;
36 | this.vertexStack = new Stack();
37 | }
38 |
39 | ///
40 | /// traverse the polygon and add triangles to the collector
41 | ///
42 | /// collector for resulting triangles
43 | public void Build(ITriangleCollector collector)
44 | {
45 | var start = this.FindStartOfMonotonePolygon();
46 | if (start >= 0)
47 | {
48 | this.TriangulateMonotonePolygon(start, collector);
49 | }
50 | else
51 | {
52 | var triangleVertices = this.polygon.SubPolygonVertices(this.subPolygonId).ToArray();
53 | collector.AddTriangle(triangleVertices[0], triangleVertices[1], triangleVertices[2]);
54 | }
55 | }
56 |
57 | ///
58 | /// Create triangles for a monotone polygon
59 | ///
60 | /// the first point (clockwise) of the long edge.
61 | /// the collector for resulting triangles
62 | private void TriangulateMonotonePolygon(int startPoint, ITriangleCollector result)
63 | {
64 | this.PullFirstTriangle(startPoint);
65 | while (true)
66 | {
67 | if (this.IsConvexCorner())
68 | {
69 | result.AddTriangle(this.current, this.third, this.second);
70 | if (!this.PopOrPullNextVertex())
71 | {
72 | return;
73 | }
74 | }
75 | else
76 | {
77 | this.PushAndPullNextVertex();
78 | }
79 | }
80 | }
81 |
82 | ///
83 | /// Gets the first three points from the triangle
84 | ///
85 | /// the start vertex, aka the target vertex of the long edge
86 | private void PullFirstTriangle(int startPoint)
87 | {
88 | this.iterator = this.polygon.IndicesStartingAt(startPoint, this.subPolygonId).GetEnumerator();
89 | this.iterator.MoveNext();
90 | this.third = this.iterator.Current;
91 | this.iterator.MoveNext();
92 | this.second = this.iterator.Current;
93 | this.iterator.MoveNext();
94 | this.current = this.iterator.Current;
95 | }
96 |
97 | ///
98 | /// Current triangle is not valid, push the third point, shift down and pull the next vertex from the polygon
99 | ///
100 | private void PushAndPullNextVertex()
101 | {
102 | this.vertexStack.Push(this.third);
103 | this.third = this.second;
104 | this.second = this.current;
105 | if (!this.iterator.MoveNext())
106 | {
107 | throw new InvalidOperationException("Triangle is incomplete");
108 | }
109 |
110 | this.current = this.iterator.Current;
111 | }
112 |
113 | ///
114 | /// either pop the last vertex from the stack or get the next vertex from the polygon
115 | ///
116 | /// true if there is one more vertex
117 | private bool PopOrPullNextVertex()
118 | {
119 | if (this.vertexStack.Count > 0)
120 | {
121 | this.second = this.third;
122 | this.third = this.vertexStack.Pop();
123 | }
124 | else if (!this.iterator.MoveNext())
125 | {
126 | return false;
127 | }
128 | else
129 | {
130 | this.second = this.current;
131 | this.current = this.iterator.Current;
132 | }
133 |
134 | return true;
135 | }
136 |
137 | ///
138 | /// Test if the current three vertices form a clockwise triangle.
139 | ///
140 | /// true if the triangle is valid
141 | private bool IsConvexCorner()
142 | {
143 | var v0 = this.vertices[this.current];
144 | var v1 = this.vertices[this.second];
145 | var v2 = this.vertices[this.third];
146 | #if UNITY_EDITOR || UNITY_STANDALONE
147 | var cross = ((v2.x - v0.x) * (v1.y - v0.y)) - ((v2.y - v0.y) * (v1.x - v0.x));
148 | #else
149 | var cross = ((v2.X - v0.X) * (v1.Y - v0.Y)) - ((v2.Y - v0.Y) * (v1.X - v0.X));
150 | #endif
151 | return cross < 0;
152 | }
153 |
154 | ///
155 | /// Find the point in the polygon that starts at the monotone side
156 | ///
157 | /// highest/lowest point in the polygon, depending if itss left hand or right hand. -1 if its a triangle.
158 | private int FindStartOfMonotonePolygon()
159 | {
160 | var startLookupIterator = this.polygon.SubPolygonVertices(this.subPolygonId).GetEnumerator();
161 | startLookupIterator.MoveNext();
162 | var first = startLookupIterator.Current;
163 | var posmax = first;
164 | var posmin = first;
165 |
166 | var movedNext = startLookupIterator.MoveNext();
167 | var posmaxNext = startLookupIterator.Current;
168 | var count = 1;
169 |
170 | while (movedNext)
171 | {
172 | var index = startLookupIterator.Current;
173 | movedNext = startLookupIterator.MoveNext();
174 |
175 | if (index > posmax)
176 | {
177 | posmax = index;
178 | posmaxNext = movedNext ? startLookupIterator.Current : first;
179 | }
180 |
181 | if (index < posmin)
182 | {
183 | posmin = index;
184 | }
185 |
186 | count++;
187 | }
188 |
189 | if (count == 3)
190 | {
191 | return -1;
192 | }
193 |
194 | if (posmin == posmaxNext)
195 | {
196 | // LHS is a single segment and it's next in the chain
197 | return posmaxNext;
198 | }
199 | else
200 | {
201 | return posmax;
202 | }
203 | }
204 | }
205 | }
206 | }
--------------------------------------------------------------------------------
/PolygonTriangulation/PolygonTriangulator.ScanSplitByTrapezoidation.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 |
7 | ///
8 | /// subclass container for triangulator
9 | ///
10 | public partial class PolygonTriangulator
11 | {
12 | ///
13 | /// Traverse the polygon, build trapezoids and collect possible splits
14 | ///
15 | private class ScanSplitByTrapezoidation : IPolygonSplitSink
16 | {
17 | private readonly Trapezoidation activeEdges;
18 | private readonly List> splits;
19 | private readonly Polygon polygon;
20 |
21 | private ScanSplitByTrapezoidation(Polygon polygon)
22 | {
23 | this.splits = new List>();
24 | this.polygon = polygon;
25 |
26 | this.activeEdges = new Trapezoidation(this.polygon.Vertices, this);
27 | }
28 |
29 | ///
30 | /// Build the splits for the polygon
31 | ///
32 | /// the polygon
33 | /// the splits
34 | public static IEnumerable> BuildSplits(Polygon polygon)
35 | {
36 | var splitter = new ScanSplitByTrapezoidation(polygon);
37 | splitter.BuildSplits(-1);
38 | return splitter.splits;
39 | }
40 |
41 | ///
42 | /// Traverse the polygon and build all splits
43 | ///
44 | /// number of steps during debugging. Use -1 for all
45 | public void BuildSplits(int stepCount)
46 | {
47 | foreach (var group in this.polygon.OrderedVertices.GroupBy(x => x.Id))
48 | {
49 | var actions = group.ToArray();
50 | if (actions.Length > 1)
51 | {
52 | actions = actions.OrderBy(x => x.Action).ToArray();
53 | }
54 |
55 | foreach (var info in actions)
56 | {
57 | if (stepCount >= 0)
58 | {
59 | stepCount -= 1;
60 | if (stepCount < 0)
61 | {
62 | return;
63 | }
64 | }
65 |
66 | switch (info.Action)
67 | {
68 | case VertexAction.ClosingCusp:
69 | this.activeEdges.HandleClosingCusp(info);
70 | break;
71 | case VertexAction.Transition:
72 | this.activeEdges.HandleTransition(info);
73 | break;
74 | case VertexAction.OpeningCusp:
75 | this.activeEdges.HandleOpeningCusp(info);
76 | break;
77 | default:
78 | throw new InvalidOperationException($"Unkown action {info.Action}");
79 | }
80 | }
81 | }
82 | }
83 |
84 | ///
85 | void IPolygonSplitSink.SplitPolygon(int leftVertex, int rightVertex)
86 | {
87 | this.splits.Add(Tuple.Create(leftVertex, rightVertex));
88 | }
89 |
90 | ///
91 | /// Run n steps and return the edges after that step
92 | ///
93 | /// the polygon
94 | /// the number of steps to run
95 | /// The edges sorted from High to Low
96 | internal static IEnumerable GetEdgesAfterPartialTrapezoidation(Polygon polygon, int depth)
97 | {
98 | var splitter = new ScanSplitByTrapezoidation(polygon);
99 | splitter.BuildSplits(depth);
100 | return splitter.activeEdges.Edges.Reverse().Select(x => x.ToString());
101 | }
102 | }
103 | }
104 | }
--------------------------------------------------------------------------------
/PolygonTriangulation/PolygonTriangulator.TriangleCollector.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System.Collections.Generic;
4 |
5 | ///
6 | /// Receive triangles
7 | ///
8 | public interface ITriangleCollector
9 | {
10 | ///
11 | /// Add a triangle
12 | ///
13 | /// id of vertex 0
14 | /// id of vertex 1
15 | /// id of vertex 2
16 | void AddTriangle(int v0, int v1, int v2);
17 | }
18 |
19 | ///
20 | /// Receive triangles and provide the recieved triangles
21 | ///
22 | public interface IArrayTriangleCollector : ITriangleCollector
23 | {
24 | ///
25 | /// Gets the triangles
26 | ///
27 | int[] Triangles { get; }
28 | }
29 |
30 | ///
31 | /// subclass container for triangulator
32 | ///
33 | public partial class PolygonTriangulator
34 | {
35 | ///
36 | /// The triangle collector
37 | ///
38 | private class TriangleCollector : IArrayTriangleCollector
39 | {
40 | private readonly List triangles;
41 |
42 | public TriangleCollector()
43 | {
44 | this.triangles = new List();
45 | }
46 |
47 | public int[] Triangles => this.triangles.ToArray();
48 |
49 | public void AddTriangle(int v0, int v1, int v2)
50 | {
51 | this.triangles.Add(v0);
52 | this.triangles.Add(v1);
53 | this.triangles.Add(v2);
54 | }
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/PolygonTriangulation/PolygonTriangulator.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 |
6 | ///
7 | /// Build triangles for a polygon
8 | ///
9 | public partial class PolygonTriangulator
10 | {
11 | private readonly Polygon polygon;
12 |
13 | ///
14 | /// Initializes a new instance of the class.
15 | ///
16 | /// the polygon to triangulate
17 | public PolygonTriangulator(Polygon polygon)
18 | {
19 | this.polygon = polygon;
20 | }
21 |
22 | ///
23 | /// Create a triangle collector
24 | ///
25 | /// a triangle collector
26 | public static IArrayTriangleCollector CreateTriangleCollector()
27 | {
28 | return new TriangleCollector();
29 | }
30 |
31 | ///
32 | /// Finally, build the triangles
33 | ///
34 | /// the triangles to represent the polygon
35 | public int[] BuildTriangles()
36 | {
37 | var collector = new TriangleCollector();
38 | this.BuildTriangles(collector);
39 | return collector.Triangles;
40 | }
41 |
42 | ///
43 | /// Finally, build the triangles
44 | ///
45 | /// the triangle collector
46 | public void BuildTriangles(ITriangleCollector collector)
47 | {
48 | var splits = ScanSplitByTrapezoidation.BuildSplits(this.polygon);
49 | var polygonWithMonotones = Polygon.Split(this.polygon, splits, collector);
50 | foreach (var subPolygonId in polygonWithMonotones.SubPolygonIds)
51 | {
52 | var triangluator = new MonotonePolygonTriangulator(polygonWithMonotones, subPolygonId);
53 | triangluator.Build(collector);
54 | }
55 | }
56 |
57 | ///
58 | /// Get the possible splits for the polygon
59 | ///
60 | /// the splits
61 | internal IEnumerable> GetSplits()
62 | {
63 | return ScanSplitByTrapezoidation.BuildSplits(this.polygon);
64 | }
65 |
66 | ///
67 | /// Iterates over the first n vertices and reports the active edges and the sort order after that step.
68 | ///
69 | /// The execution depth.
70 | /// sorted active edges
71 | internal IEnumerable GetEdgesAfterPartialTrapezoidation(int depth)
72 | {
73 | return ScanSplitByTrapezoidation.GetEdgesAfterPartialTrapezoidation(this.polygon, depth);
74 | }
75 | }
76 | }
--------------------------------------------------------------------------------
/PolygonTriangulation/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // Allgemeine Informationen über eine Assembly werden über die folgenden
6 | // Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern,
7 | // die einer Assembly zugeordnet sind.
8 | [assembly: AssemblyTitle("PolygonTriangulation")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("PolygonTriangulation")]
13 | [assembly: AssemblyCopyright("Copyright © 2020")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Durch Festlegen von ComVisible auf FALSE werden die Typen in dieser Assembly
18 | // für COM-Komponenten unsichtbar. Wenn Sie auf einen Typ in dieser Assembly von
19 | // COM aus zugreifen müssen, sollten Sie das ComVisible-Attribut für diesen Typ auf "True" festlegen.
20 | [assembly: ComVisible(false)]
21 |
22 | // Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird
23 | [assembly: Guid("250832d7-a812-4b2a-9ef7-f1791ec657ff")]
24 |
25 | // Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten:
26 | //
27 | // Hauptversion
28 | // Nebenversion
29 | // Buildnummer
30 | // Revision
31 | //
32 | // Sie können alle Werte angeben oder Standardwerte für die Build- und Revisionsnummern verwenden,
33 | // indem Sie "*" wie unten gezeigt eingeben:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
38 | [assembly: InternalsVisibleTo("TriangulationTests")]
39 | [assembly: InternalsVisibleTo("PolygonDisplay")]
--------------------------------------------------------------------------------
/PolygonTriangulation/RedBlackTree.DumpEnumerator.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System.Collections;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 |
7 | ///
8 | /// subclass container for redblacktree
9 | ///
10 | public sealed partial class RedBlackTree
11 | {
12 | ///
13 | /// Dump enumeration support. Prints one line per tree level with proper spacing.
14 | ///
15 | private class DumpEnumerator : IEnumerable
16 | {
17 | private readonly RedBlackTree tree;
18 | private readonly int configuredDept;
19 |
20 | public DumpEnumerator(RedBlackTree tree, int maxDepth)
21 | {
22 | this.tree = tree;
23 | this.configuredDept = maxDepth;
24 | }
25 |
26 | ///
27 | public IEnumerator GetEnumerator()
28 | {
29 | foreach (var line in this.Dump())
30 | {
31 | yield return line;
32 | }
33 | }
34 |
35 | ///
36 | IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
37 |
38 | ///
39 | /// Enumerate values, their red/black state and their depth. Used only to dump the tree.
40 | ///
41 | /// the current node
42 | /// the level of node
43 | /// the maximum allowed level
44 | /// tuple of value, red/black state, node depth
45 | private static IEnumerable<(T data, string color, int level)> EnumerateLevels(Node node, int level, int maxDepth)
46 | {
47 | Node left;
48 | Node right;
49 | if (node == null)
50 | {
51 | if (maxDepth < 0 || level > maxDepth)
52 | {
53 | yield break;
54 | }
55 |
56 | left = null;
57 | right = null;
58 | yield return (default(T), "_", level);
59 | }
60 | else
61 | {
62 | left = node.Left;
63 | right = node.Right;
64 | yield return (node.Data, node.ColorText, level);
65 | }
66 |
67 | foreach (var item in EnumerateLevels(left, level + 1, maxDepth))
68 | {
69 | yield return item;
70 | }
71 |
72 | foreach (var item in EnumerateLevels(right, level + 1, maxDepth))
73 | {
74 | yield return item;
75 | }
76 | }
77 |
78 | ///
79 | /// Create one line for each level and space the items suitable for 2 digit numbers
80 | ///
81 | /// array of strings, one line per level
82 | private IEnumerable Dump()
83 | {
84 | var maxDepth = this.configuredDept;
85 | if (maxDepth < 0)
86 | {
87 | if (((ICollection)this.tree).Count == 0)
88 | {
89 | return Enumerable.Empty();
90 | }
91 |
92 | maxDepth = EnumerateLevels(this.tree.root, 0, -1).Select(x => x.level).Max();
93 | }
94 |
95 | var groups = EnumerateLevels(this.tree.root, 0, maxDepth)
96 | .GroupBy(x => x.level)
97 | .OrderBy(x => x.Key)
98 | .ToArray();
99 |
100 | var height = groups.Length;
101 | var total = 1 << height;
102 | var itemLenght = 4 + 1;
103 |
104 | return groups.Select(g =>
105 | {
106 | var spacingFactor = total / (1 << g.Key) / 2;
107 | var spacing = new string(' ', ((itemLenght + 1) * (spacingFactor - 1)) + 1);
108 | var left = new string(' ', (spacing.Length - 1) / 2);
109 | var rest = string.Join(spacing, g.Select(x => ((Equals(x.data, default(T)) && !char.IsLower(x.color[0])) ? "- " : $"{x.data}{x.color}").PadLeft(itemLenght)));
110 | return left + rest;
111 | });
112 | }
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/PolygonTriangulation/RedBlackTree.Node.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System;
4 | using System.Diagnostics;
5 |
6 | ///
7 | /// subclass container for redblacktree
8 | ///
9 | public sealed partial class RedBlackTree
10 | {
11 | ///
12 | /// Tree node
13 | ///
14 | [DebuggerDisplay("{Data}{ColorText}")]
15 | private class Node : IOrderedNode
16 | {
17 | private Color color;
18 |
19 | ///
20 | /// Initializes a new instance of the class.
21 | ///
22 | /// The data associated with the node.
23 | public Node(T data)
24 | {
25 | this.Data = data;
26 | this.color = Color.Red;
27 | }
28 |
29 | ///
30 | /// The color of the node.
31 | ///
32 | private enum Color
33 | {
34 | ///
35 | /// Node is red - do not count for black height
36 | ///
37 | Red,
38 |
39 | ///
40 | /// Node is black - count for black height and rebalance on delete
41 | ///
42 | Black,
43 |
44 | ///
45 | /// Node is double black. Can be inherited from child only.
46 | ///
47 | DoubleBlackNode,
48 |
49 | ///
50 | /// A black leaf was deleted, rebalance the parent tree and remove this node afterwards.
51 | ///
52 | DoubleBlackNull,
53 | }
54 |
55 | ///
56 | /// Gets a value indicating whether is is the root node
57 | ///
58 | public bool IsTop => this.Parent == null;
59 |
60 | ///
61 | /// Gets a value indicating whether this.parent.left == this
62 | ///
63 | public bool IsLeft => this.Parent?.Left == this;
64 |
65 | ///
66 | /// Gets a value indicating whether this.parent.right == this
67 | ///
68 | public bool IsRight => this.Parent?.Right == this;
69 |
70 | ///
71 | public T Data { get; }
72 |
73 | ///
74 | /// Gets or sets a value indicating whether the node is red (true) or black (false)
75 | ///
76 | public bool IsRed
77 | {
78 | get => this.color == Color.Red;
79 | set
80 | {
81 | if (this.color == Color.DoubleBlackNull)
82 | {
83 | throw new InvalidOperationException("Can't change the DeletedBlack color");
84 | }
85 |
86 | this.color = value ? Color.Red : Color.Black;
87 | }
88 | }
89 |
90 | ///
91 | /// Gets a value indicating whether the state is double black.
92 | ///
93 | public bool IsDoubleBlack => this.color == Color.DoubleBlackNode || this.color == Color.DoubleBlackNull;
94 |
95 | ///
96 | /// Gets the parent node.
97 | ///
98 | public Node Parent { get; private set; }
99 |
100 | ///
101 | /// Gets the left node.
102 | ///
103 | public Node Left { get; private set; }
104 |
105 | ///
106 | /// Gets the right node.
107 | ///
108 | public Node Right { get; private set; }
109 |
110 | ///
111 | public IOrderedNode NextNode
112 | {
113 | get
114 | {
115 | Node node;
116 | if (this.Right != null)
117 | {
118 | for (node = this.Right; node.Left != null; node = node.Left)
119 | {
120 | // just iterate
121 | }
122 | }
123 | else
124 | {
125 | for (node = this; node.IsRight; node = node.Parent)
126 | {
127 | // just iterate
128 | }
129 |
130 | node = node.Parent;
131 | }
132 |
133 | return node;
134 | }
135 | }
136 |
137 | ///
138 | public IOrderedNode PrevNode
139 | {
140 | get
141 | {
142 | Node node;
143 | if (this.Left != null)
144 | {
145 | for (node = this.Left; node.Right != null; node = node.Right)
146 | {
147 | // just iterate
148 | }
149 | }
150 | else
151 | {
152 | for (node = this; node.IsLeft; node = node.Parent)
153 | {
154 | // just iterate
155 | }
156 |
157 | node = node.Parent;
158 | }
159 |
160 | return node;
161 | }
162 | }
163 |
164 | ///
165 | /// Gets the color as text - debug only.
166 | ///
167 | internal string ColorText
168 | {
169 | get
170 | {
171 | switch (this.color)
172 | {
173 | case Color.Red:
174 | return "R";
175 | case Color.Black:
176 | return "B";
177 | case Color.DoubleBlackNode:
178 | return "b";
179 | case Color.DoubleBlackNull:
180 | return "x";
181 | default:
182 | return "_";
183 | }
184 | }
185 | }
186 |
187 | ///
188 | /// Get's the other node with the same parent
189 | ///
190 | /// the sibling of the item
191 | public Node GetSibling()
192 | {
193 | if (this.Parent == null)
194 | {
195 | return null;
196 | }
197 |
198 | return this.IsLeft ? this.Parent.Right : this.Parent.Left;
199 | }
200 |
201 | ///
202 | /// Set this node as root node
203 | ///
204 | /// the root variable
205 | public void SetRoot(ref Node root)
206 | {
207 | root = this;
208 | this.Parent = null;
209 | this.color = Color.Black;
210 | }
211 |
212 | ///
213 | /// Flip the red flag
214 | ///
215 | public void FlipRed()
216 | {
217 | this.IsRed = !this.IsRed;
218 | }
219 |
220 | ///
221 | /// Set the left child. Same as SetChild(true, child)
222 | ///
223 | /// the new child
224 | public void SetLeftChild(Node child)
225 | {
226 | this.Left = child;
227 | if (child != null)
228 | {
229 | child.Parent = this;
230 | }
231 | }
232 |
233 | ///
234 | /// Set the right child. Same as SetChild(false, child)
235 | ///
236 | /// the new child
237 | public void SetRightChild(Node child)
238 | {
239 | this.Right = child;
240 | if (child != null)
241 | {
242 | child.Parent = this;
243 | }
244 | }
245 |
246 | ///
247 | /// Set the child node either as left or right
248 | ///
249 | /// choose left or right node
250 | /// the new child
251 | public void SetChild(bool atLeft, Node child)
252 | {
253 | if (atLeft)
254 | {
255 | this.Left = child;
256 | }
257 | else
258 | {
259 | this.Right = child;
260 | }
261 |
262 | if (child != null)
263 | {
264 | child.Parent = this;
265 | }
266 | }
267 |
268 | ///
269 | /// Exchanges this color with the color of the peer
270 | ///
271 | /// the other
272 | public void SwapRedFlag(Node peer)
273 | {
274 | if (this.color != peer.color)
275 | {
276 | peer.color = this.color;
277 | this.FlipRed();
278 | }
279 | }
280 |
281 | ///
282 | /// Replace the deleted child with a new value.
283 | /// Create a special value for double black situations.
284 | /// Handles red/black/doubleblack state of replaced child.
285 | ///
286 | /// the flag if child is left of it's parent.
287 | /// the child to replace
288 | /// the replacement node (a child of child)
289 | /// The replaced valued. If child.Black + replacement==null, a special value
290 | public Node ReplaceDeletedChild(bool atLeft, Node child, Node replacement)
291 | {
292 | if (!child.IsRed)
293 | {
294 | if (replacement == null)
295 | {
296 | replacement = new Node(default)
297 | {
298 | color = Color.DoubleBlackNull,
299 | };
300 | }
301 | else if (replacement.IsRed)
302 | {
303 | replacement.IsRed = false;
304 | }
305 | else
306 | {
307 | throw new InvalidOperationException("replacement can't be black because replacement.parent.left == null");
308 | }
309 | }
310 | else if (replacement?.IsRed == false)
311 | {
312 | throw new InvalidOperationException("child and replacement are red, violated precondition");
313 | }
314 |
315 | this.SetChild(atLeft, replacement);
316 | return replacement;
317 | }
318 |
319 | ///
320 | /// Remove the double black state
321 | ///
322 | public void ResolveDoubleBlackState()
323 | {
324 | if (!this.IsDoubleBlack)
325 | {
326 | throw new InvalidOperationException("Don't call double black operation if not double black");
327 | }
328 |
329 | if (this.Parent == null)
330 | {
331 | throw new InvalidOperationException("The parent should never take the double black state");
332 | }
333 |
334 | if (this.Parent.IsRed)
335 | {
336 | this.Parent.IsRed = false;
337 | }
338 |
339 | if (this.color == Color.DoubleBlackNull)
340 | {
341 | this.Parent.SetChild(this.IsLeft, null);
342 | }
343 | else
344 | {
345 | this.color = Color.Black;
346 | }
347 | }
348 |
349 | ///
350 | /// The child has a double black state and can't resolve it. Take it over.
351 | ///
352 | ///
353 | /// The root node doesn't need to take double black.
354 | ///
355 | public void InheritDoubleBlackStateFromChild()
356 | {
357 | if (this.Parent != null)
358 | {
359 | this.color = this.color == Color.Red ? Color.Black : Color.DoubleBlackNode;
360 | }
361 | }
362 | }
363 | }
364 | }
365 |
--------------------------------------------------------------------------------
/PolygonTriangulation/Trapezoidation.EdgeComparer.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System.Collections.Generic;
4 | using System.Runtime.CompilerServices;
5 |
6 | #if UNITY_EDITOR || UNITY_STANDALONE
7 | using Vertex = UnityEngine.Vector2;
8 | #else
9 | using Vertex = System.Numerics.Vector2;
10 | #endif
11 |
12 | ///
13 | /// subclass container for trapzoidation
14 | ///
15 | public partial class Trapezoidation
16 | {
17 | ///
18 | /// Compares two edges
19 | ///
20 | private class EdgeComparer : IComparer
21 | {
22 | private readonly IReadOnlyList vertices;
23 |
24 | ///
25 | /// Initializes a new instance of the class.
26 | ///
27 | /// the real vertices referenced by vertex ids
28 | public EdgeComparer(IReadOnlyList vertices)
29 | {
30 | this.vertices = vertices;
31 | }
32 |
33 | ///
34 | /// Test if the left vertex of x is above y.
35 | ///
36 | /// the current added value
37 | /// the edge that is already part of the tree
38 | /// a comparison result
39 | public int Compare(TrapezoidEdge x, TrapezoidEdge y)
40 | {
41 | var value = x;
42 | var storage = y;
43 | var vertexOfValue = value.Left == storage.Left ? value.Right : value.Left;
44 | return this.IsVertexAbove(vertexOfValue, storage) ? 1 : -1;
45 | }
46 |
47 | ///
48 | /// Test if the ordering of the edges is correct, both edges have a common point on the left
49 | ///
50 | /// the lower edge
51 | /// the upper edge
52 | /// true if upper is above lower
53 | ///
54 | /// take the wider edge (larger X span) to avoid a large slope.
55 | ///
56 | public bool EdgeOrderingWithCommonLeftIsCorrect(TrapezoidEdge lower, TrapezoidEdge upper)
57 | {
58 | var left = this.vertices[upper.Left];
59 | var upperRight = this.vertices[upper.Right];
60 | var lowerRight = this.vertices[lower.Right];
61 |
62 | #if UNITY_EDITOR || UNITY_STANDALONE
63 | var leftY = left.y;
64 | var upperRightX = upperRight.x;
65 | var upperRightY = upperRight.y;
66 | var lowerRightX = lowerRight.x;
67 | var lowerRightY = lowerRight.y;
68 | #else
69 | var leftY = left.Y;
70 | var upperRightX = upperRight.X;
71 | var upperRightY = upperRight.Y;
72 | var lowerRightX = lowerRight.X;
73 | var lowerRightY = lowerRight.Y;
74 | #endif
75 |
76 | if ((upperRightY > leftY) != (lowerRightY > leftY))
77 | {
78 | return upperRightY > lowerRightY;
79 | }
80 |
81 | if (upperRightX > lowerRightX)
82 | {
83 | if (upperRightY < leftY && upperRightY > lowerRightY)
84 | {
85 | return true;
86 | }
87 | else if (upperRightY > leftY && upperRightY < lowerRightY)
88 | {
89 | return false;
90 | }
91 | else
92 | {
93 | return !IsVertexAboveSlow(ref lowerRight, ref left, ref upperRight);
94 | }
95 | }
96 | else
97 | {
98 | if (lowerRightY > leftY && upperRightY > lowerRightY)
99 | {
100 | return true;
101 | }
102 | else if (lowerRightY < leftY && upperRightY < lowerRightY)
103 | {
104 | return false;
105 | }
106 | else
107 | {
108 | return IsVertexAboveSlow(ref upperRight, ref left, ref lowerRight);
109 | }
110 | }
111 | }
112 |
113 | ///
114 | /// Test if the vertex is above this edge by calculating the edge.Y at vertex.X
115 | ///
116 | /// The vertex.
117 | /// The left vertex of the edge.
118 | /// The right vertex of the edge.
119 | /// true if the verex is above
120 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
121 | private static bool IsVertexAboveSlow(ref Vertex vertex, ref Vertex left, ref Vertex right)
122 | {
123 | #if UNITY_EDITOR || UNITY_STANDALONE
124 | var xSpan = right.x - left.x;
125 |
126 | if (xSpan < epsilon * epsilon)
127 | {
128 | return vertex.y > left.y;
129 | }
130 |
131 | var yOfEdgeAtVertex = (vertex.x - left.x) / xSpan * (right.y - left.y) + left.y;
132 | return yOfEdgeAtVertex < vertex.y;
133 | #else
134 | var xSpan = right.X - left.X;
135 |
136 | if (xSpan < Epsilon * Epsilon)
137 | {
138 | return vertex.Y > left.Y;
139 | }
140 |
141 | var yOfEdgeAtVertex = ((vertex.X - left.X) / xSpan * (right.Y - left.Y)) + left.Y;
142 | return yOfEdgeAtVertex < vertex.Y;
143 | #endif
144 | }
145 |
146 | ///
147 | /// Test if the vertex is above the line that is formed by the edge
148 | ///
149 | /// The vertex identifier.
150 | /// The edge.
151 | /// true if the vertex is above the edge
152 | ///
153 | /// This is called only during insert operations, therefore value.left is larger than storage.left.
154 | /// Try to find the result without calculation first, then calculate the storage.Y at value.Left.X
155 | ///
156 | private bool IsVertexAbove(int vertexId, TrapezoidEdge edge)
157 | {
158 | var vertex = this.vertices[vertexId];
159 | var left = this.vertices[edge.Left];
160 | var right = this.vertices[edge.Right];
161 |
162 | #if UNITY_EDITOR || UNITY_STANDALONE
163 | // this is very likely as the points are added in order left to right
164 | if (vertex.x >= left.x)
165 | {
166 | if (vertex.y > left.y)
167 | {
168 | if (left.y >= right.y || (vertex.x < right.x && vertex.y > right.y))
169 | {
170 | return true;
171 | }
172 | }
173 | else
174 | {
175 | if (left.y < right.y || (vertex.x < right.x && vertex.y < right.y))
176 | {
177 | return false;
178 | }
179 | }
180 | }
181 | #else
182 | // this is very likely as the points are added in order left to right
183 | if (vertex.X >= left.X)
184 | {
185 | if (vertex.Y > left.Y)
186 | {
187 | if (left.Y >= right.Y || (vertex.X < right.X && vertex.Y > right.Y))
188 | {
189 | return true;
190 | }
191 | }
192 | else
193 | {
194 | if (left.Y < right.Y || (vertex.X < right.X && vertex.Y < right.Y))
195 | {
196 | return false;
197 | }
198 | }
199 | }
200 | #endif
201 |
202 | return IsVertexAboveSlow(ref vertex, ref left, ref right);
203 | }
204 | }
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/PolygonTriangulation/Trapezoidation.Trapezoid.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System;
4 | using System.Diagnostics;
5 |
6 | ///
7 | /// subclass container for trapzoidation
8 | ///
9 | public partial class Trapezoidation
10 | {
11 | ///
12 | /// Create trapezoids by splitting, joining and traversing along a polygon.
13 | /// Each trapezoid has a left and a right base and an upper and lower edge.
14 | /// The left/right base lines are vertical, parallel and the X coordinate is defined by the associated vertex.
15 | /// Trapezoids are built from left to right.
16 | /// Each edge, the upper and the lower, has one current trapezoid.
17 | ///
18 | [DebuggerDisplay("{Debug}")]
19 | private class Trapezoid
20 | {
21 | ///
22 | /// Gets the upper edge
23 | ///
24 | private readonly TrapezoidEdge upperEdge;
25 |
26 | ///
27 | /// Gets the lower edge
28 | ///
29 | private readonly TrapezoidEdge lowerEdge;
30 |
31 | ///
32 | /// The neighbor state of the left base and .
33 | ///
34 | private readonly Base leftBase;
35 |
36 | ///
37 | /// Gets the index of the left vertex, defining the left base.
38 | ///
39 | private readonly int leftVertex;
40 |
41 | ///
42 | /// Initializes a new instance of the class.
43 | ///
44 | /// the id of the vertex for the left "virtual trapezoid" edge
45 | /// The state of the left base.
46 | /// the lower edge
47 | /// the upper edge
48 | private Trapezoid(int leftVertex, Base leftBase, TrapezoidEdge lowerEdge, TrapezoidEdge upperEdge)
49 | {
50 | this.leftBase = leftBase;
51 | this.leftVertex = leftVertex;
52 |
53 | this.lowerEdge = lowerEdge;
54 | this.upperEdge = upperEdge;
55 | }
56 |
57 | ///
58 | /// The neighbor state of the left/right base line
59 | ///
60 | [Flags]
61 | private enum Base
62 | {
63 | ///
64 | /// No neighbor, i.e. a triangle.
65 | ///
66 | NoNeighbor = 1,
67 |
68 | ///
69 | /// One neighbor and the common vertex is on the upper corner.
70 | ///
71 | UpperCorner = 2,
72 |
73 | ///
74 | /// One neighbor and the common vertex is on the lower corner.
75 | ///
76 | LowerCorner = 4,
77 |
78 | ///
79 | /// The baseline has two neighbors, the associated vertex is somewhere in the middle of the base.
80 | ///
81 | TwoNeighbors = 8,
82 | }
83 |
84 | ///
85 | /// Gets a debug string
86 | ///
87 | public string Debug => $"Left:{this.leftVertex} {this.leftBase} Low: {this.lowerEdge} High: {this.upperEdge}";
88 |
89 | ///
90 | /// A left pointing cusp that enters the polygon space.
91 | ///
92 | /// the vertex id of the cusp
93 | /// the lower edge of the new split
94 | /// the upper edge of the new split
95 | public static void EnterInsideBySplit(int vertexId, TrapezoidEdge lowerEdge, TrapezoidEdge upperEdge)
96 | {
97 | UpdateEdges(new Trapezoid(vertexId, Base.NoNeighbor, lowerEdge, upperEdge));
98 | }
99 |
100 | ///
101 | /// A right pointing cusp that enters the polygon space. Join the upper left and the lower left trapezoids in one.
102 | ///
103 | /// the left lower trapezoid
104 | /// the left upper trapezoid
105 | /// the vertex id that joins the two edges.
106 | /// the polygon splitter
107 | public static void EnterInsideByJoin(Trapezoid lower, Trapezoid upper, int vertexId, IPolygonSplitSink splitSink)
108 | {
109 | upper.EvaluateRight(vertexId, Base.LowerCorner, splitSink);
110 | lower.EvaluateRight(vertexId, Base.UpperCorner, splitSink);
111 |
112 | UpdateEdges(new Trapezoid(vertexId, Base.TwoNeighbors, lower.lowerEdge, upper.upperEdge));
113 | }
114 |
115 | ///
116 | /// A cusp that transitions from inside to outside. Splits the Trapezoid by one point.
117 | ///
118 | /// the vertex id of the start point
119 | /// the lower edge of the new split
120 | /// the upper edge of the new split
121 | /// the polygon splitter
122 | public void LeaveInsideBySplit(int vertexId, TrapezoidEdge lowerEdge, TrapezoidEdge upperEdge, IPolygonSplitSink splitSink)
123 | {
124 | this.EvaluateRight(vertexId, Base.TwoNeighbors, splitSink);
125 |
126 | UpdateEdges(new Trapezoid(vertexId, Base.LowerCorner, upperEdge, this.upperEdge));
127 | UpdateEdges(new Trapezoid(vertexId, Base.UpperCorner, this.lowerEdge, lowerEdge));
128 | }
129 |
130 | ///
131 | /// Join two edges. Right of the vertex is outside.
132 | ///
133 | /// the closing vertex id
134 | /// the polygon splitter
135 | public void LeaveInsideByJoin(int vertexId, IPolygonSplitSink splitSink)
136 | {
137 | this.EvaluateRight(vertexId, Base.NoNeighbor, splitSink);
138 | }
139 |
140 | ///
141 | /// The upper edge transitions at vertex to a new edge
142 | ///
143 | /// the transition vertex
144 | /// the new edge
145 | /// the polygon splitter
146 | public void TransitionOnUpperEdge(int vertexId, TrapezoidEdge nextEdge, IPolygonSplitSink splitSink)
147 | {
148 | this.EvaluateRight(vertexId, Base.UpperCorner, splitSink);
149 |
150 | UpdateEdges(new Trapezoid(vertexId, Base.UpperCorner, this.lowerEdge, nextEdge));
151 | }
152 |
153 | ///
154 | /// The lower edge transitions at vertex to a new edge
155 | ///
156 | /// the transition vertex
157 | /// the new edge
158 | /// the polygon splitter
159 | public void TransitionOnLowerEdge(int vertexId, TrapezoidEdge nextEdge, IPolygonSplitSink splitSink)
160 | {
161 | this.EvaluateRight(vertexId, Base.LowerCorner, splitSink);
162 |
163 | UpdateEdges(new Trapezoid(vertexId, Base.LowerCorner, nextEdge, this.upperEdge));
164 | }
165 |
166 | ///
167 | /// Detects whether the left and right vertex represent a diagonale of the trapezoid.
168 | ///
169 | /// the combined base line state
170 | /// true if a diagonale is detected
171 | private static bool DetectDiagonale(Base combinedBase)
172 | {
173 | return combinedBase == (Base.LowerCorner | Base.UpperCorner);
174 | }
175 |
176 | ///
177 | /// Update the edges to point to the new trapezoid
178 | ///
179 | /// the trapezoid
180 | private static void UpdateEdges(Trapezoid trapezoid)
181 | {
182 | trapezoid.lowerEdge.Trapezoid = trapezoid;
183 | trapezoid.upperEdge.Trapezoid = trapezoid;
184 | }
185 |
186 | ///
187 | /// Detects whether one side has two neighbors (i.e. a touching cusp).
188 | ///
189 | /// the combined base line state
190 | /// true if any base line has two neighbors
191 | private static bool DetectDoubleNeighbor(Base combinedBase)
192 | {
193 | return (combinedBase & Base.TwoNeighbors) != 0;
194 | }
195 |
196 | ///
197 | /// Combine the right side info with the left side and evaluate if it matches a split situation.
198 | ///
199 | /// the vertex that defines the right side
200 | /// the number of right neighbors
201 | /// the sink for split information
202 | private void EvaluateRight(int rightVertex, Base rightBase, IPolygonSplitSink splitter)
203 | {
204 | var combinedBase = this.leftBase | rightBase;
205 | if (DetectDoubleNeighbor(combinedBase) || DetectDiagonale(combinedBase))
206 | {
207 | splitter.SplitPolygon(this.leftVertex, rightVertex);
208 | }
209 | }
210 | }
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/PolygonTriangulation/Trapezoidation.TrapezoidEdge.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | ///
4 | /// subclass container for trapzoidation
5 | ///
6 | public partial class Trapezoidation
7 | {
8 | ///
9 | /// Internal representation of an edge
10 | ///
11 | private class TrapezoidEdge : ITestingTrapezoidEdge
12 | {
13 | public TrapezoidEdge(int left, int right, int rightUnique, bool isRightToLeft)
14 | {
15 | this.IsRightToLeft = isRightToLeft;
16 | this.Left = left;
17 | this.Right = right;
18 | this.RightUnique = rightUnique;
19 | }
20 |
21 | ///
22 | /// Gets a value indicating whether this instance is right to left.
23 | ///
24 | public bool IsRightToLeft { get; }
25 |
26 | ///
27 | /// Gets the left vertex id.
28 | ///
29 | public int Left { get; }
30 |
31 | ///
32 | /// Gets the right vertex id.
33 | ///
34 | public int Right { get; }
35 |
36 | ///
37 | /// Gets the unique id of the right vertex
38 | ///
39 | public int RightUnique { get; }
40 |
41 | ///
42 | /// Gets or sets the node in the red black tree
43 | ///
44 | public IOrderedNode TreeNode { get; set; }
45 |
46 | ///
47 | /// Gets or sets the current associated trapezoid.
48 | ///
49 | public Trapezoid Trapezoid { get; set; }
50 |
51 | ///
52 | public override string ToString()
53 | {
54 | return $"{this.Left}{(this.IsRightToLeft ? "<" : ">")}{this.Right}";
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/PolygonTriangulation/Trapezoidation.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 |
6 | #if UNITY_EDITOR || UNITY_STANDALONE
7 | using Vertex = UnityEngine.Vector2;
8 | #else
9 | using Vertex = System.Numerics.Vector2;
10 | #endif
11 |
12 | ///
13 | /// The receiver of split commands
14 | ///
15 | public interface IPolygonSplitSink
16 | {
17 | ///
18 | /// Split the polygon between left and right vertex
19 | ///
20 | /// the left vertex
21 | /// the right vertex
22 | void SplitPolygon(int leftVertex, int rightVertex);
23 | }
24 |
25 | ///
26 | /// Marker interface for trapezoid edge tests
27 | ///
28 | internal interface ITestingTrapezoidEdge
29 | {
30 | }
31 |
32 | ///
33 | /// Splits a polygon into trapezoids and reports necessary splits.
34 | ///
35 | public partial class Trapezoidation
36 | {
37 | private const float Epsilon = 1.0E-5f;
38 |
39 | ///
40 | /// A comparer for trapezoid edges vs. vertex
41 | ///
42 | private readonly EdgeComparer comparer;
43 |
44 | ///
45 | /// Store the active adges with prev/next support
46 | ///
47 | private readonly RedBlackTree activeEdges;
48 |
49 | ///
50 | /// map the right of the vertex to the active polygon edge. Collisions for closing vertices ar handled by
51 | ///
52 | private readonly Dictionary vertexToEdge;
53 |
54 | ///
55 | /// the receiver for the detected splits
56 | ///
57 | private readonly IPolygonSplitSink splitSink;
58 |
59 | ///
60 | /// Initializes a new instance of the class.
61 | ///
62 | /// The vertices.
63 | /// The sink for detected splits.
64 | public Trapezoidation(IReadOnlyList vertices, IPolygonSplitSink splitSink)
65 | {
66 | this.vertexToEdge = new Dictionary();
67 | this.comparer = new EdgeComparer(vertices);
68 | this.activeEdges = new RedBlackTree(this.comparer);
69 | this.splitSink = splitSink;
70 | }
71 |
72 | ///
73 | /// Gets all edges starting from the lowest
74 | ///
75 | internal IEnumerable Edges => this.activeEdges.Items;
76 |
77 | ///
78 | /// Handle an opening cusp. i.e. starts two new edges.
79 | ///
80 | /// The vertex information.
81 | public void HandleOpeningCusp(IPolygonVertexInfo info)
82 | {
83 | var (lowerEdge, upperEdge) = this.StartNewTrapezoidEdges(
84 | info.Id,
85 | info.PrevVertexId,
86 | info.PrevUnique,
87 | info.NextVertexId,
88 | info.NextUnique);
89 | if (lowerEdge.IsRightToLeft)
90 | {
91 | Trapezoid.EnterInsideBySplit(info.Id, lowerEdge, upperEdge);
92 | }
93 | else
94 | {
95 | var belowEdge = lowerEdge.TreeNode.PrevNode.Data;
96 | var trapezoid = belowEdge.Trapezoid;
97 | trapezoid.LeaveInsideBySplit(info.Id, lowerEdge, upperEdge, this.splitSink);
98 | }
99 | }
100 |
101 | ///
102 | /// Handle a closing cusp. i.e. joins two edges
103 | ///
104 | /// The vertex information.
105 | public void HandleClosingCusp(IPolygonVertexInfo info)
106 | {
107 | var lowerEdge = this.EdgeForVertex(info.Id, info.Unique);
108 | TrapezoidEdge upperEdge;
109 |
110 | var prevEdge = lowerEdge.TreeNode.PrevNode?.Data;
111 | if (prevEdge?.Right == lowerEdge.Right && (prevEdge.Left == info.PrevVertexId || prevEdge.Left == info.NextVertexId))
112 | {
113 | upperEdge = lowerEdge;
114 | lowerEdge = prevEdge;
115 | }
116 | else
117 | {
118 | upperEdge = lowerEdge.TreeNode.NextNode?.Data;
119 | }
120 |
121 | if (lowerEdge.Right != upperEdge?.Right)
122 | {
123 | throw new InvalidOperationException($"Invalid join of edges lower: {lowerEdge} and upper: {upperEdge}");
124 | }
125 |
126 | var lowerTrapezoid = lowerEdge.Trapezoid;
127 | if (lowerEdge.IsRightToLeft)
128 | {
129 | lowerTrapezoid.LeaveInsideByJoin(info.Id, this.splitSink);
130 | }
131 | else
132 | {
133 | var upperEdge2 = lowerEdge.TreeNode.NextNode.Data;
134 | var upperTrapezoid = upperEdge2.Trapezoid;
135 | Trapezoid.EnterInsideByJoin(lowerTrapezoid, upperTrapezoid, info.Id, this.splitSink);
136 | }
137 |
138 | this.JoinTrapezoidEdges(lowerEdge);
139 | }
140 |
141 | ///
142 | /// A transition from one vertex to the next, where prev>id has the same direction as id>next
143 | ///
144 | /// The vertex information.
145 | public void HandleTransition(IPolygonVertexInfo info)
146 | {
147 | var oldEdge = this.EdgeForVertex(info.Id, info.Unique);
148 | var trapezoid = oldEdge.Trapezoid;
149 | if (oldEdge.IsRightToLeft)
150 | {
151 | var newEdge = this.Transition(oldEdge, info.PrevVertexId, info.PrevUnique);
152 | trapezoid.TransitionOnLowerEdge(info.Id, newEdge, this.splitSink);
153 | }
154 | else
155 | {
156 | var newEdge = this.Transition(oldEdge, info.NextVertexId, info.NextUnique);
157 | trapezoid.TransitionOnUpperEdge(info.Id, newEdge, this.splitSink);
158 | }
159 | }
160 |
161 | ///
162 | /// Insert two edges starting in one point.
163 | ///
164 | /// the index of the starting vertex
165 | /// the end index of the lower edge
166 | /// the end index of the upper edge
167 | /// (lower edge, upper edge)
168 | ///
169 | /// There is never a Begin() where the prev or next is left to start or prev is at same X and below start.
170 | ///
171 | internal (ITestingTrapezoidEdge lower, ITestingTrapezoidEdge upper) TestBegin(int start, int prev, int next)
172 | {
173 | return this.StartNewTrapezoidEdges(start, prev, prev, next, next);
174 | }
175 |
176 | ///
177 | /// transition from one edge to the next
178 | ///
179 | /// the previous edge
180 | /// the new target vertex id
181 | /// the new edge
182 | internal ITestingTrapezoidEdge TestTransition(ITestingTrapezoidEdge edge, int newTarget)
183 | {
184 | if (edge is TrapezoidEdge trapezoidEdge)
185 | {
186 | return this.Transition(trapezoidEdge, newTarget, newTarget);
187 | }
188 |
189 | throw new InvalidOperationException("Invalid use of internal test function");
190 | }
191 |
192 | ///
193 | /// Two edges join in a final vertex
194 | ///
195 | /// the lower edge
196 | internal void TestJoin(ITestingTrapezoidEdge lower)
197 | {
198 | if (lower is TrapezoidEdge trapezoidEdge)
199 | {
200 | this.JoinTrapezoidEdges(trapezoidEdge);
201 | return;
202 | }
203 |
204 | throw new InvalidOperationException("Invalid use of internal test function");
205 | }
206 |
207 | ///
208 | /// Insert two edges starting in one point.
209 | ///
210 | /// the id of the common start vertex (left)
211 | /// the end vertex of the lower edge
212 | /// the uniqe id of the prev vertex
213 | /// the end vertex of the upper edge
214 | /// the uniqe id of the next vertex
215 | /// (lower edge, upper edge)
216 | ///
217 | /// There is never a Begin() where the prev or next is left to start or prev is at same X and below start.
218 | ///
219 | private (TrapezoidEdge lower, TrapezoidEdge upper) StartNewTrapezoidEdges(int start, int prev, int prevUnique, int next, int nextUnique)
220 | {
221 | var lower = new TrapezoidEdge(start, prev, prevUnique, true);
222 | var upper = new TrapezoidEdge(start, next, nextUnique, false);
223 |
224 | if (!this.comparer.EdgeOrderingWithCommonLeftIsCorrect(lower, upper))
225 | {
226 | (lower, upper) = (upper, lower);
227 | }
228 |
229 | (lower.TreeNode, upper.TreeNode) = this.activeEdges.AddPair(lower, upper);
230 |
231 | this.StoreEdge(lower);
232 | this.StoreEdge(upper);
233 |
234 | return (lower, upper);
235 | }
236 |
237 | ///
238 | /// transition from one edge to the next
239 | ///
240 | /// the existing edge
241 | /// the new target vertex
242 | /// the uniqe id of newTarget
243 | /// the new edge
244 | private TrapezoidEdge Transition(TrapezoidEdge edge, int newTarget, int targetUnique)
245 | {
246 | var nextEdge = new TrapezoidEdge(edge.Right, newTarget, targetUnique, edge.IsRightToLeft)
247 | {
248 | Trapezoid = edge.Trapezoid,
249 | };
250 |
251 | nextEdge.TreeNode = this.activeEdges.ReplaceNode(edge.TreeNode, nextEdge);
252 |
253 | this.vertexToEdge.Remove(edge.RightUnique);
254 | this.StoreEdge(nextEdge);
255 |
256 | return nextEdge;
257 | }
258 |
259 | ///
260 | /// Two edges join in a final vertex
261 | ///
262 | /// the lower edge
263 | private void JoinTrapezoidEdges(TrapezoidEdge lowerEdge)
264 | {
265 | var nextNode = lowerEdge.TreeNode.NextNode;
266 | this.activeEdges.RemoveNode(nextNode);
267 | this.activeEdges.RemoveNode(lowerEdge.TreeNode);
268 |
269 | this.vertexToEdge.Remove(lowerEdge.RightUnique);
270 | }
271 |
272 | ///
273 | /// Store the edge for direct lookup
274 | ///
275 | /// the edge
276 | private void StoreEdge(TrapezoidEdge edge)
277 | {
278 | this.vertexToEdge[edge.RightUnique] = edge;
279 | }
280 |
281 | ///
282 | /// Gets the active edge with right point == vertexId. If there are two edges, return the lower one.
283 | ///
284 | /// the vertex id
285 | /// the uniqe id of the vertex
286 | /// the edge
287 | private TrapezoidEdge EdgeForVertex(int vertexId, int vertexUniqe)
288 | {
289 | if (!this.vertexToEdge.TryGetValue(vertexUniqe, out var edge))
290 | {
291 | throw new InvalidOperationException($"Can't find edge for vertex {vertexId} at {vertexUniqe}");
292 | }
293 |
294 | return edge;
295 | }
296 | }
297 | }
298 |
--------------------------------------------------------------------------------
/PolygonTriangulation/TriangulationException.cs:
--------------------------------------------------------------------------------
1 | namespace PolygonTriangulation
2 | {
3 | using System;
4 | using System.Linq;
5 | using System.Runtime.Serialization;
6 |
7 | ///
8 | /// polygon data for exceptions during polygon triangulation
9 | ///
10 | [Serializable]
11 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1032:Standardausnahmekonstruktoren implementieren", Justification = "Exception must have a polygon")]
12 | public class TriangulationException : InvalidOperationException
13 | {
14 | ///
15 | /// Initializes a new instance of the class.
16 | ///
17 | /// The polygon.
18 | /// The edge create code.
19 | /// The inner exception.
20 | public TriangulationException(Polygon polygon, string edgeCreateCode, Exception innerException)
21 | : this(polygon, edgeCreateCode, innerException.Message, innerException)
22 | {
23 | }
24 |
25 | ///
26 | /// Initializes a new instance of the class.
27 | ///
28 | /// The polygon.
29 | /// The edge create code.
30 | /// The message.
31 | /// The inner exception.
32 | public TriangulationException(Polygon polygon, string edgeCreateCode, string message, Exception innerException)
33 | : base(message, innerException)
34 | {
35 | this.PolygonCreateCode = BuildPolygonCode(polygon);
36 | this.EdgeCreateCode = edgeCreateCode;
37 | }
38 |
39 | ///
40 | /// Initializes a new instance of the class. Used during deserialization.
41 | ///
42 | /// The object that holds the serialized object data.
43 | /// The contextual information about the source or destination.
44 | protected TriangulationException(SerializationInfo info, StreamingContext context)
45 | : base(info, context)
46 | {
47 | this.EdgeCreateCode = info.GetString(nameof(this.EdgeCreateCode));
48 | this.PolygonCreateCode = info.GetString(nameof(this.PolygonCreateCode));
49 | }
50 |
51 | ///
52 | /// Gets the code to feed the edges to a polygon builder.
53 | ///
54 | public string EdgeCreateCode { get; }
55 |
56 | ///
57 | /// Gets the code to create the polygon in a unittest
58 | ///
59 | public string PolygonCreateCode { get; }
60 |
61 | ///
62 | public override void GetObjectData(SerializationInfo info, StreamingContext context)
63 | {
64 | base.GetObjectData(info, context);
65 | info.AddValue(nameof(this.EdgeCreateCode), this.EdgeCreateCode);
66 | info.AddValue(nameof(this.PolygonCreateCode), this.PolygonCreateCode);
67 | }
68 |
69 | ///
70 | /// Create a polygon string that can be used to create the polygon
71 | ///
72 | /// The polygon to convert to code.
73 | /// polygon as code
74 | internal static string BuildPolygonCode(Polygon polygon)
75 | {
76 | if (polygon == null)
77 | {
78 | return string.Empty;
79 | }
80 |
81 | var sb = new System.Text.StringBuilder();
82 | var culture = System.Globalization.CultureInfo.InvariantCulture;
83 |
84 | sb.AppendLine("var vertices = new[]");
85 | sb.AppendLine("{");
86 | #if UNITY_EDITOR || UNITY_STANDALONE
87 | var vertexStrings = polygon.Vertices.Select(
88 | x => string.Format(culture, " new Vertex({0:0.0000000}f, {1:0.0000000}f),", x.x, x.y));
89 | #else
90 | var vertexStrings = polygon.Vertices.Select(
91 | x => string.Format(culture, " new Vertex({0:0.0000000}f, {1:0.0000000}f),", x.X, x.Y));
92 | #endif
93 | sb.AppendLine(string.Join(Environment.NewLine, vertexStrings));
94 | sb.AppendLine("};");
95 | sb.AppendLine(string.Empty);
96 |
97 | sb.AppendLine("var polygon = Polygon.Build(vertices)");
98 | foreach (var subPolygonId in polygon.SubPolygonIds)
99 | {
100 | sb.AppendLine($" .AddVertices({string.Join(", ", polygon.SubPolygonVertices(subPolygonId))})");
101 | sb.AppendLine($" .ClosePartialPolygon()");
102 | }
103 |
104 | var fusionVerticex = polygon.SubPolygonIds
105 | .SelectMany(x => polygon.SubPolygonVertices(x))
106 | .GroupBy(x => x)
107 | .Where(x => x.Count() > 1)
108 | .Select(x => x.Key);
109 | sb.AppendLine($" .Close({string.Join(", ", fusionVerticex)});");
110 |
111 | return sb.ToString();
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | [](https://opensource.org/licenses/MIT)
2 | [](https://github.com/git-ruttmann/PolygonTriangulation/actions?query=workflow%3A%22.NET+Core%22)
3 | [](https://app.codacy.com/manual/git-ruttmann/PolygonTriangulation)
4 | [](https://www.codacy.com/manual/git-ruttmann/PolygonTriangulation?utm_source=github.com&utm_medium=referral&utm_content=git-ruttmann/PolygonTriangulation&utm_campaign=Badge_Coverage)
5 |
6 | # Polygon Triangulation
7 |
8 | A C# library to convert a complex polygon into triangles.
9 | Support includes:
10 | * multiple polygons
11 | * polygon holes
12 | * fusion vertex (same vertex is used by two sub polygons)
13 |
14 | The project was developed as part of a Unity project to visualize the intersection between a 3D mesh and a plane.
15 |
16 | The included Windows Forms project `PolygonDisplay` visualizes the [Polygon](Documentation/Polygon.md), the detected splits and the [Monotones](Documentation/Monotones.md).
17 |
18 | ## Usage
19 |
20 | Create a `PlanePolygonBuilder(plane)` and call repeatedly `AddEdge(start, end)`.
21 | After adding the last edge, call `Build()`.
22 | The result contains a vertex array and a list of Triangles. A Triangle is defined by three consecutive indices in the vertex array.
23 |
24 | The 3D vertices of the edge are converted to 2D by rotating them along the plane and removing the depth component.
25 |
26 | ## Unity
27 |
28 | Unity is using its own types for Vector2, Vector3, Plane and Quaternion, while the common code uses thy types from `System.Numerics`.
29 | The `#if UNITY_EDITOR || UNITY_STANDALONE` directive changes the namespace and the different case for the property names like `Vector2.x` vs `Vector2.X`.
30 |
31 | To use the code, copy all files from PolygonTriangulation to the unity scripts folder.
32 |
33 | ## Testing
34 |
35 | To test the intermediate steps, create a `IEdgesToPolygonBuilder` by calling `PlanePolygonBuilder.CreatePolygonBuilder()`.
36 | Call `AddEdge(start, end)` to add the polygon edges and finally `BuildPolygon()` to receive a `IPlanePolygon` which contains a
37 | [Polygon](Documentation/Polygon.md) and the original 3D vertices.
38 | The polygon contains the 2D vertices in the same order as the 3D vertices of the result.
39 |
40 | Alternatively construct the [Polygon](Documentation/Polygon.md) with `Polygon.Build()`.
41 |
42 | Create a new `PolygonTriangulator(polygon)` and call `BuildTriangles()`.
43 | That uses [Trapezoidation](Documentation/Polygon.md) to split the [Polygon](Documentation/Polygon.md) into [Monotones](Documentation/Monotones.md) and then
44 | triangulates those monotones.
45 |
46 | To test only the edges to polygon conversion, create a `IPolygonLineDetector` by calling `PlanePolygonBuilder.CreatePolygonLineDetector()`.
47 | Call `JoinEdgesToPolygones(pairsOfEdges)` to create polygon lines. It results in lists of `ClosedPolygons` and `UnclosedPolygons`.
48 |
49 | Unclosed polygons can be joined by `TryClusteringUnclosedEnds(vertices)`.
50 | That merges unclosed polygons by considering vertex coordinates with small distances as the same vertex.
51 |
--------------------------------------------------------------------------------
/TriangulationTests/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.InteropServices;
3 |
4 | [assembly: AssemblyTitle("TriangulationTests")]
5 | [assembly: AssemblyDescription("")]
6 | [assembly: AssemblyConfiguration("")]
7 | [assembly: AssemblyCompany("")]
8 | [assembly: AssemblyProduct("TriangulationTests")]
9 | [assembly: AssemblyCopyright("Copyright © 2020")]
10 | [assembly: AssemblyTrademark("")]
11 | [assembly: AssemblyCulture("")]
12 |
13 | [assembly: ComVisible(false)]
14 |
15 | [assembly: Guid("981fe2f3-4d3e-4f7f-8a64-32fd485330bb")]
16 |
17 | // [assembly: AssemblyVersion("1.0.*")]
18 | [assembly: AssemblyVersion("1.0.0.0")]
19 | [assembly: AssemblyFileVersion("1.0.0.0")]
20 |
--------------------------------------------------------------------------------
/TriangulationTests/TriangulationTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netcoreapp3.1
4 | false
5 |
6 |
7 | DEBUG;TRACE
8 |
9 |
10 |
11 |
12 |
13 |
14 | runtime; build; native; contentfiles; analyzers; buildtransitive
15 | all
16 |
17 |
18 | all
19 | runtime; build; native; contentfiles; analyzers; buildtransitive
20 |
21 |
22 |
23 |
24 |
25 | all
26 | runtime; build; native; contentfiles; analyzers; buildtransitive
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/TriangulationTests/UnitTest1.cs:
--------------------------------------------------------------------------------
1 | namespace TriangulationTests
2 | {
3 | using System;
4 | using System.Linq;
5 |
6 | using Microsoft.VisualStudio.TestTools.UnitTesting;
7 | using PolygonTriangulation;
8 |
9 | using Vertex = System.Numerics.Vector2;
10 |
11 | ///
12 | /// More unittests that do not match a specific group.
13 | ///
14 | [TestClass]
15 | public class UnitTest1
16 | {
17 | ///
18 | /// Add and Remove items from a sorted edges list
19 | ///
20 | [TestMethod]
21 | public void SortedEdges()
22 | {
23 | var vertices = new[]
24 | {
25 | new Vertex(1, 1), // 0
26 | new Vertex(1, 3),
27 | new Vertex(1.5f, 3), // 2
28 | new Vertex(2, 2),
29 | new Vertex(2, 4), // 4
30 | new Vertex(2.5f, 1),
31 | new Vertex(2.5f, 2), // 6
32 | new Vertex(2.5f, 3),
33 | new Vertex(3.5f, 2.5f), // 8
34 | new Vertex(3.5f, 1),
35 | new Vertex(4, 1.5f), // 10
36 | new Vertex(4, 3.5f),
37 | new Vertex(4, 4), // 12
38 | };
39 |
40 | var sorted = new Trapezoidation(vertices, new SplitCollector());
41 | var (b0, b0Upper) = sorted.TestBegin(0, 5, 6);
42 | Assert.AreEqual("0<5 0>6", string.Join(" ", sorted.Edges));
43 |
44 | var (b1, b1Upper) = sorted.TestBegin(1, 3, 4);
45 | Assert.AreEqual("0<5 0>6 1<3 1>4", string.Join(" ", sorted.Edges));
46 |
47 | var (b2, b2Upper) = sorted.TestBegin(2, 7, 12);
48 | Assert.AreEqual("0<5 0>6 1<3 2<7 2>12 1>4", string.Join(" ", sorted.Edges));
49 |
50 | sorted.TestTransition(b1, 6);
51 | Assert.AreEqual("0<5 0>6 3<6 2<7 2>12 1>4", string.Join(" ", sorted.Edges));
52 |
53 | sorted.TestTransition(b1Upper, 12);
54 | Assert.AreEqual("0<5 0>6 3<6 2<7 2>12 4>12", string.Join(" ", sorted.Edges));
55 |
56 | var t5 = sorted.TestTransition(b0, 9);
57 | Assert.AreEqual("5<9 0>6 3<6 2<7 2>12 4>12", string.Join(" ", sorted.Edges));
58 |
59 | sorted.TestJoin(b0Upper);
60 | Assert.AreEqual("5<9 2<7 2>12 4>12", string.Join(" ", sorted.Edges));
61 |
62 | sorted.TestTransition(b2, 11);
63 | Assert.AreEqual("5<9 7<11 2>12 4>12", string.Join(" ", sorted.Edges));
64 |
65 | var (_, b8Upper) = sorted.TestBegin(8, 10, 11);
66 | Assert.AreEqual("5<9 8<10 8>11 7<11 2>12 4>12", string.Join(" ", sorted.Edges));
67 |
68 | var t9 = sorted.TestTransition(t5, 10);
69 | Assert.AreEqual("9<10 8<10 8>11 7<11 2>12 4>12", string.Join(" ", sorted.Edges));
70 |
71 | sorted.TestJoin(t9);
72 | Assert.AreEqual("8>11 7<11 2>12 4>12", string.Join(" ", sorted.Edges));
73 |
74 | sorted.TestJoin(b8Upper);
75 | Assert.AreEqual("2>12 4>12", string.Join(" ", sorted.Edges));
76 |
77 | sorted.TestJoin(b2Upper);
78 | Assert.AreEqual(string.Empty, string.Join(" ", sorted.Edges));
79 | }
80 |
81 | ///
82 | /// Test the sorting of edges with quickpath
83 | ///
84 | [TestMethod]
85 | public void OpeningCuspEdgeSorting()
86 | {
87 | var vertices = new[]
88 | {
89 | new Vertex(0, 0), // 0
90 | new Vertex(2, -2),
91 | new Vertex(2, 2), // 2
92 | new Vertex(3, -6),
93 | new Vertex(3, 6), // 4
94 | new Vertex(6, -3),
95 | new Vertex(6, -1), // 6
96 | new Vertex(6, 1),
97 | new Vertex(6, 3),
98 | };
99 |
100 | var tests = new[]
101 | {
102 | (3, 1, true),
103 | (1, 5, true),
104 | (1, 6, true),
105 | (6, 7, true),
106 | (7, 2, true),
107 | (8, 2, true),
108 | (8, 4, true),
109 | (2, 4, true),
110 |
111 | (1, 3, false),
112 | (5, 1, false),
113 | (6, 1, false),
114 | (7, 6, false),
115 | (2, 8, false),
116 | (2, 7, false),
117 | (4, 8, false),
118 | (4, 2, false),
119 | };
120 |
121 | foreach (var (prev, next, orderIsCorrect) in tests)
122 | {
123 | var trapezoidation = new Trapezoidation(vertices, new SplitCollector());
124 | trapezoidation.TestBegin(0, prev, next);
125 | if (orderIsCorrect)
126 | {
127 | Assert.AreEqual($"0<{prev} 0>{next}", string.Join(" ", trapezoidation.Edges), $"Bad order after {prev}>0>{next}");
128 | }
129 | else
130 | {
131 | Assert.AreEqual($"0>{next} 0<{prev}", string.Join(" ", trapezoidation.Edges), $"Bad reordering after {prev}>0>{next}");
132 | }
133 | }
134 | }
135 |
136 | ///
137 | /// Create a polygon and then create the code for the polygon
138 | ///
139 | [TestMethod]
140 | public void CreateTriangulationException()
141 | {
142 | var sortedVertices = new[]
143 | {
144 | new Vertex(0, 0),
145 | new Vertex(1, 2),
146 | new Vertex(1, 3), // 2
147 | new Vertex(2, 2),
148 | new Vertex(3, 3), // 4
149 | new Vertex(4, 2),
150 | new Vertex(5, 2),
151 | new Vertex(5, 3), // 7
152 | new Vertex(6, 1),
153 | };
154 |
155 | var sourcePolygon = Polygon.Build(sortedVertices)
156 | .AddVertices(0, 2, 4, 7, 8)
157 | .ClosePartialPolygon()
158 | .AddVertices(4, 5, 6)
159 | .Close(4);
160 |
161 | var innerException = new InvalidOperationException("Inner Exception");
162 | var outerException = new TriangulationException(sourcePolygon, "edge creation code", innerException);
163 |
164 | Assert.AreEqual(outerException.InnerException, innerException);
165 | Assert.AreEqual("Inner Exception", outerException.Message);
166 | Assert.IsNotNull(outerException.PolygonCreateCode);
167 | Assert.AreEqual("edge creation code", outerException.EdgeCreateCode);
168 | }
169 |
170 | ///
171 | /// Create a polygon and then create the code for the polygon
172 | ///
173 | [TestMethod]
174 | public void CreatePolygonCode()
175 | {
176 | var sortedVertices = new[]
177 | {
178 | new Vertex(0, 0),
179 | new Vertex(1, 2),
180 | new Vertex(1, 3), // 2
181 | new Vertex(2, 2),
182 | new Vertex(3, 3), // 4
183 | new Vertex(4, 2),
184 | new Vertex(5, 2),
185 | new Vertex(5, 3), // 7
186 | new Vertex(6, 1),
187 | };
188 |
189 | var sourcePolygon = Polygon.Build(sortedVertices)
190 | .AddVertices(0, 2, 4, 7, 8)
191 | .ClosePartialPolygon()
192 | .AddVertices(4, 5, 6)
193 | .Close(4);
194 |
195 | var code = TriangulationException.BuildPolygonCode(sourcePolygon);
196 |
197 | var expected = @"var vertices = new[]
198 | {
199 | new Vertex(0.0000000f, 0.0000000f),
200 | new Vertex(1.0000000f, 2.0000000f),
201 | new Vertex(1.0000000f, 3.0000000f),
202 | new Vertex(2.0000000f, 2.0000000f),
203 | new Vertex(3.0000000f, 3.0000000f),
204 | new Vertex(4.0000000f, 2.0000000f),
205 | new Vertex(5.0000000f, 2.0000000f),
206 | new Vertex(5.0000000f, 3.0000000f),
207 | new Vertex(6.0000000f, 1.0000000f),
208 | };
209 |
210 | var polygon = Polygon.Build(vertices)
211 | .AddVertices(2, 4, 5, 6, 4, 7, 8, 0)
212 | .ClosePartialPolygon()
213 | .Close(4);";
214 |
215 | var lines = code.Split("\r\n".ToCharArray(), StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim());
216 | var expectedLines = expected.Split("\r\n".ToCharArray(), StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim());
217 | CollectionAssert.AreEqual(lines.ToArray(), expectedLines.ToArray());
218 | }
219 |
220 | ///
221 | /// a dummy split collector
222 | ///
223 | private class SplitCollector : IPolygonSplitSink
224 | {
225 | void IPolygonSplitSink.SplitPolygon(int leftVertex, int rightVertex)
226 | {
227 | // dummy collector
228 | }
229 | }
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/TriangulationTests/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------