Skip to content

Commit 30ea41b

Browse files
Merge pull request #19 from SixLabors/js/api-cleanup
Update API to match DotNet Conventions
2 parents 02624b8 + 8481a30 commit 30ea41b

20 files changed

+316
-318
lines changed

PolygonClipper.sln

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio Version 17
4-
VisualStudioVersion = 17.12.35707.178 d17.12
4+
VisualStudioVersion = 17.12.35707.178
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PolygonClipper", "src\PolygonClipper\PolygonClipper.csproj", "{3C8D945E-6074-437E-B6EA-237BD0C80411}"
77
EndProject
@@ -74,4 +74,8 @@ Global
7474
GlobalSection(ExtensibilityGlobals) = postSolution
7575
SolutionGuid = {2C73BEEC-091B-45E4-A0BD-7D7CD16A8451}
7676
EndGlobalSection
77+
GlobalSection(SharedMSBuildProjectFiles) = preSolution
78+
shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{3c8d945e-6074-437e-b6ea-237bd0c80411}*SharedItemsImports = 5
79+
shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13
80+
EndGlobalSection
7781
EndGlobal

README.md

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,30 @@
22

33
[![License: Six Labors Split](https://img.shields.io/badge/license-Six%20Labors%20Split-%23e30183)](https://github.com/SixLabors/PolygonClipper/blob/master/LICENSE)
44

5-
## A Simple Algorithm for Boolean Operations on Polygons
5+
A C# implementation of the Martínez–Rueda algorithm for performing Boolean operations on polygons. This library supports union, intersection, difference, and xor operations on complex polygons with holes, multiple contours, and self-intersections.
66

7-
*Francisco Martínez, Carlos Ogayar, Juan R. Jiménez, Antonio J. Rueda*
8-
9-
https://sci-hub.se/10.1016/j.advengsoft.2013.04.004
7+
## Features
108

11-
This repository contains the beginnings of an attempted port of the original public domain C++ implementation by the main author of the paper Francisco Martínez.
9+
- Works with non-convex polygons, including holes and multiple disjoint regions
10+
- Handles edge cases like overlapping edges and vertical segments
11+
- Preserves topology: output polygons include hole/contour hierarchy
12+
- Deterministic and robust sweep line algorithm with O((n + k) log n) complexity
1213

13-
The original code can be found in the reference folder.
14-
15-
The plan is to implement a performant port, add additional tests and some method by which to generate renders of output clipping operations.
16-
17-
This is currently an intellectual exercise but I believe a C# port could be very useful in many applications if proven successful.
18-
19-
All and any assistance is gratefully accepted. :heart:
14+
## Usage
15+
16+
The API centers around `Polygon` and `Contour` types. Construct input polygons using contours, then apply Boolean operations via the `PolygonClipper` class:
17+
18+
```csharp
19+
Polygon result = PolygonClipper.Intersect(subject, clipping);
20+
```
21+
22+
## Based On
23+
24+
This implementation is based on the algorithm described in:
25+
26+
> F. Martínez et al., "A simple algorithm for Boolean operations on polygons", *Advances in Engineering Software*, 64 (2013), pp. 11–19.
27+
> https://sci-hub.se/10.1016/j.advengsoft.2013.04.004
28+
29+
## License
30+
31+
Six Labors Split License. See [`LICENSE`](https://github.com/SixLabors/PolygonClipper/blob/main/LICENSE) for details.

src/PolygonClipper/Contour.cs

Lines changed: 56 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4+
using System.Collections;
45
using System.Diagnostics;
56
using System.Runtime.CompilerServices;
67

@@ -9,89 +10,94 @@ namespace SixLabors.PolygonClipper;
910
/// <summary>
1011
/// Represents a simple polygon. The edges of the contours are interior disjoint.
1112
/// </summary>
12-
[DebuggerDisplay("Count = {VertexCount}")]
13-
public sealed class Contour
13+
[DebuggerDisplay("Count = {Count}")]
14+
#pragma warning disable CA1710 // Identifiers should have correct suffix
15+
public sealed class Contour : IReadOnlyCollection<Vertex>
16+
#pragma warning restore CA1710 // Identifiers should have correct suffix
1417
{
15-
private bool precomputeCC;
16-
private bool cc;
18+
private bool hasCachedOrientation;
19+
private bool cachedCounterClockwise;
1720

1821
/// <summary>
19-
/// Set of points conforming the external contour
22+
/// Set of vertices conforming the external contour
2023
/// </summary>
21-
private readonly List<Vertex> points = [];
24+
private readonly List<Vertex> vertices = [];
2225

2326
/// <summary>
2427
/// Holes of the contour. They are stored as the indexes of
2528
/// the holes in a polygon class
2629
/// </summary>
27-
private readonly List<int> holes = [];
30+
private readonly List<int> holeIndices = [];
2831

2932
/// <summary>
3033
/// Gets the number of vertices.
3134
/// </summary>
32-
public int VertexCount => this.points.Count;
33-
34-
/// <summary>
35-
/// Gets the number of edges.
36-
/// </summary>
37-
public int EdgeCount => this.points.Count;
35+
public int Count
36+
{
37+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
38+
get => this.vertices.Count;
39+
}
3840

3941
/// <summary>
4042
/// Gets the number of holes.
4143
/// </summary>
42-
public int HoleCount => this.holes.Count;
44+
public int HoleCount => this.holeIndices.Count;
4345

4446
/// <summary>
4547
/// Gets a value indicating whether the contour is external (not a hole).
4648
/// </summary>
47-
public bool IsExternal => this.HoleOf == null;
49+
public bool IsExternal => this.ParentIndex == null;
4850

4951
/// <summary>
50-
/// Gets or sets the ID of the parent contour if this contour is a hole.
52+
/// Gets or sets the index of the parent contour in the polygon if this contour is a hole.
5153
/// </summary>
52-
public int? HoleOf { get; set; }
54+
public int? ParentIndex { get; set; }
5355

5456
/// <summary>
5557
/// Gets or sets the depth of the contour.
5658
/// </summary>
5759
public int Depth { get; set; }
5860

5961
/// <summary>
60-
/// Gets the vertex at the specified index of the external contour.
62+
/// Gets the vertex at the specified index.
6163
/// </summary>
6264
/// <param name="index">The index of the vertex.</param>
63-
/// <returns>The <see cref="Vertex"/>.</returns>
64-
public Vertex GetVertex(int index) => this.points[index];
65+
/// <returns>The <see cref="Vertex"/> at the specified index.</returns>
66+
public Vertex this[int index]
67+
{
68+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
69+
get => this.vertices[index];
70+
}
6571

6672
/// <summary>
6773
/// Gets the hole index at the specified position in the contour.
6874
/// </summary>
6975
/// <param name="index">The index of the hole.</param>
7076
/// <returns>The hole index.</returns>
71-
public int GetHoleIndex(int index) => this.holes[index];
77+
public int GetHoleIndex(int index) => this.holeIndices[index];
7278

7379
/// <summary>
7480
/// Gets the segment at the specified index of the contour.
7581
/// </summary>
7682
/// <param name="index">The index of the segment.</param>
7783
/// <returns>The <see cref="Segment"/>.</returns>
78-
internal Segment Segment(int index)
79-
=> (index == this.VertexCount - 1)
80-
? new Segment(this.points[^1], this.points[0])
81-
: new Segment(this.points[index], this.points[index + 1]);
84+
internal Segment GetSegment(int index)
85+
=> (index == this.Count - 1)
86+
? new Segment(this.vertices[^1], this.vertices[0])
87+
: new Segment(this.vertices[index], this.vertices[index + 1]);
8288

8389
/// <summary>
8490
/// Gets the bounding box of the contour.
8591
/// </summary>
8692
/// <returns>The <see cref="Box2"/>.</returns>
8793
public Box2 GetBoundingBox()
8894
{
89-
if (this.VertexCount == 0)
95+
if (this.Count == 0)
9096
{
9197
return default;
9298
}
9399

94-
List<Vertex> points = this.points;
100+
List<Vertex> points = this.vertices;
95101
Box2 b = new(points[0]);
96102
for (int i = 1; i < points.Count; ++i)
97103
{
@@ -109,18 +115,18 @@ public Box2 GetBoundingBox()
109115
/// </returns>
110116
public bool IsCounterClockwise()
111117
{
112-
if (this.precomputeCC)
118+
if (this.hasCachedOrientation)
113119
{
114-
return this.cc;
120+
return this.cachedCounterClockwise;
115121
}
116122

117-
this.precomputeCC = true;
123+
this.hasCachedOrientation = true;
118124

119125
double area = 0;
120126
Vertex c;
121127
Vertex c1;
122128

123-
List<Vertex> points = this.points;
129+
List<Vertex> points = this.vertices;
124130
for (int i = 0; i < points.Count - 1; i++)
125131
{
126132
c = points[i];
@@ -131,7 +137,7 @@ public bool IsCounterClockwise()
131137
c = points[^1];
132138
c1 = points[0];
133139
area += Vertex.Cross(c, c1);
134-
return this.cc = area >= 0;
140+
return this.cachedCounterClockwise = area >= 0;
135141
}
136142

137143
/// <summary>
@@ -147,8 +153,8 @@ public bool IsCounterClockwise()
147153
/// </summary>
148154
public void Reverse()
149155
{
150-
this.points.Reverse();
151-
this.cc = !this.cc;
156+
this.vertices.Reverse();
157+
this.cachedCounterClockwise = !this.cachedCounterClockwise;
152158
}
153159

154160
/// <summary>
@@ -180,7 +186,7 @@ public void SetCounterClockwise()
180186
/// <param name="y">The y-coordinate offset.</param>
181187
public void Translate(double x, double y)
182188
{
183-
List<Vertex> points = this.points;
189+
List<Vertex> points = this.vertices;
184190
for (int i = 0; i < points.Count; i++)
185191
{
186192
points[i] += new Vertex(x, y);
@@ -192,38 +198,46 @@ public void Translate(double x, double y)
192198
/// </summary>
193199
/// <param name="vertex">The vertex to add.</param>
194200
[MethodImpl(MethodImplOptions.AggressiveInlining)]
195-
public void AddVertex(in Vertex vertex) => this.points.Add(vertex);
201+
public void AddVertex(in Vertex vertex) => this.vertices.Add(vertex);
196202

197203
/// <summary>
198204
/// Removes the vertex at the specified index from the contour.
199205
/// </summary>
200206
/// <param name="index">The index of the vertex to remove.</param>
201207
[MethodImpl(MethodImplOptions.AggressiveInlining)]
202-
public void RemoveVertexAt(int index) => this.points.RemoveAt(index);
208+
public void RemoveVertexAt(int index) => this.vertices.RemoveAt(index);
203209

204210
/// <summary>
205211
/// Clears all vertices and holes from the contour.
206212
/// </summary>
207213
public void Clear()
208214
{
209-
this.points.Clear();
210-
this.holes.Clear();
215+
this.vertices.Clear();
216+
this.holeIndices.Clear();
211217
}
212218

213219
/// <summary>
214220
/// Clears all holes from the contour.
215221
/// </summary>
216-
public void ClearHoles() => this.holes.Clear();
222+
public void ClearHoles() => this.holeIndices.Clear();
217223

218224
/// <summary>
219225
/// Gets the last vertex in the contour.
220226
/// </summary>
221227
/// <returns>The last <see cref="Vertex"/> in the contour.</returns>
222-
public Vertex GetLastVertex() => this.points[^1];
228+
public Vertex GetLastVertex() => this.vertices[^1];
223229

224230
/// <summary>
225231
/// Adds a hole index to the contour.
226232
/// </summary>
227233
/// <param name="index">The index of the hole to add.</param>
228-
public void AddHoleIndex(int index) => this.holes.Add(index);
234+
public void AddHoleIndex(int index) => this.holeIndices.Add(index);
235+
236+
/// <inheritdoc/>
237+
public IEnumerator<Vertex> GetEnumerator()
238+
=> ((IEnumerable<Vertex>)this.vertices).GetEnumerator();
239+
240+
/// <inheritdoc/>
241+
IEnumerator IEnumerable.GetEnumerator()
242+
=> ((IEnumerable)this.vertices).GetEnumerator();
229243
}

0 commit comments

Comments
 (0)