Skip to content

Commit f9528e2

Browse files
Cleanup API, update readme
1 parent 46f0eb1 commit f9528e2

File tree

7 files changed

+173
-193
lines changed

7 files changed

+173
-193
lines changed

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: 57 additions & 43 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.Collections.Generic;
56
using System.Diagnostics;
67
using System.Runtime.CompilerServices;
@@ -10,89 +11,94 @@ namespace PolygonClipper;
1011
/// <summary>
1112
/// Represents a simple polygon. The edges of the contours are interior disjoint.
1213
/// </summary>
13-
[DebuggerDisplay("Count = {VertexCount}")]
14-
public sealed class Contour
14+
[DebuggerDisplay("Count = {Count}")]
15+
#pragma warning disable CA1710 // Identifiers should have correct suffix
16+
public sealed class Contour : IReadOnlyCollection<Vertex>
17+
#pragma warning restore CA1710 // Identifiers should have correct suffix
1518
{
16-
private bool precomputeCC;
17-
private bool cc;
19+
private bool hasCachedOrientation;
20+
private bool cachedCounterClockwise;
1821

1922
/// <summary>
20-
/// Set of points conforming the external contour
23+
/// Set of vertices conforming the external contour
2124
/// </summary>
22-
private readonly List<Vertex> points = [];
25+
private readonly List<Vertex> vertices = [];
2326

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

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

4042
/// <summary>
4143
/// Gets the number of holes.
4244
/// </summary>
43-
public int HoleCount => this.holes.Count;
45+
public int HoleCount => this.holeIndices.Count;
4446

4547
/// <summary>
4648
/// Gets a value indicating whether the contour is external (not a hole).
4749
/// </summary>
48-
public bool IsExternal => this.HoleOf == null;
50+
public bool IsExternal => this.ParentIndex == null;
4951

5052
/// <summary>
51-
/// Gets or sets the ID of the parent contour if this contour is a hole.
53+
/// Gets or sets the index of the parent contour in the polygon if this contour is a hole.
5254
/// </summary>
53-
public int? HoleOf { get; set; }
55+
public int? ParentIndex { get; set; }
5456

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

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

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

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

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

95-
List<Vertex> points = this.points;
101+
List<Vertex> points = this.vertices;
96102
Box2 b = new(points[0]);
97103
for (int i = 1; i < points.Count; ++i)
98104
{
@@ -110,18 +116,18 @@ public Box2 GetBoundingBox()
110116
/// </returns>
111117
public bool IsCounterClockwise()
112118
{
113-
if (this.precomputeCC)
119+
if (this.hasCachedOrientation)
114120
{
115-
return this.cc;
121+
return this.cachedCounterClockwise;
116122
}
117123

118-
this.precomputeCC = true;
124+
this.hasCachedOrientation = true;
119125

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

124-
List<Vertex> points = this.points;
130+
List<Vertex> points = this.vertices;
125131
for (int i = 0; i < points.Count - 1; i++)
126132
{
127133
c = points[i];
@@ -132,7 +138,7 @@ public bool IsCounterClockwise()
132138
c = points[^1];
133139
c1 = points[0];
134140
area += Vertex.Cross(c, c1);
135-
return this.cc = area >= 0;
141+
return this.cachedCounterClockwise = area >= 0;
136142
}
137143

138144
/// <summary>
@@ -148,8 +154,8 @@ public bool IsCounterClockwise()
148154
/// </summary>
149155
public void Reverse()
150156
{
151-
this.points.Reverse();
152-
this.cc = !this.cc;
157+
this.vertices.Reverse();
158+
this.cachedCounterClockwise = !this.cachedCounterClockwise;
153159
}
154160

155161
/// <summary>
@@ -181,7 +187,7 @@ public void SetCounterClockwise()
181187
/// <param name="y">The y-coordinate offset.</param>
182188
public void Translate(double x, double y)
183189
{
184-
List<Vertex> points = this.points;
190+
List<Vertex> points = this.vertices;
185191
for (int i = 0; i < points.Count; i++)
186192
{
187193
points[i] += new Vertex(x, y);
@@ -193,38 +199,46 @@ public void Translate(double x, double y)
193199
/// </summary>
194200
/// <param name="vertex">The vertex to add.</param>
195201
[MethodImpl(MethodImplOptions.AggressiveInlining)]
196-
public void AddVertex(in Vertex vertex) => this.points.Add(vertex);
202+
public void AddVertex(in Vertex vertex) => this.vertices.Add(vertex);
197203

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

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

214220
/// <summary>
215221
/// Clears all holes from the contour.
216222
/// </summary>
217-
public void ClearHoles() => this.holes.Clear();
223+
public void ClearHoles() => this.holeIndices.Clear();
218224

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

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

0 commit comments

Comments
 (0)