├── .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 | Monotones 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 | Monotone A Monotone B 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 | Monotone Three 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 | Before single split 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 | Before single split Monotone A 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 | Hole integrated by split Hole after full split 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 | Split after join 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 | Single Fusion Single Fusion 130 | Single Fusion Single Fusion 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 | Good Fusion Order 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 | Bad Fusion Order 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 | Single Fusion 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 | Basic Polygon 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 | Trapezoid Lines 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 | Trapezoids 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 | Trapezoid Splits 68 | 69 | And finally the [Monotone Polygons](Monotones.md), simple triangles are grey: 70 | 71 | Monotones 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