Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion PolygonClipper.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35707.178 d17.12
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we switch to the slnx format?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn’t that require the latest NET 9 SDK to build?

I’ll need to investigate. Will leave for now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah you are right. But building .net8 with sdk 9 should be possible.

We can just wait and I can put it on my list for things after November(net10 release) 😅

VisualStudioVersion = 17.12.35707.178
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PolygonClipper", "src\PolygonClipper\PolygonClipper.csproj", "{3C8D945E-6074-437E-B6EA-237BD0C80411}"
EndProject
Expand Down Expand Up @@ -74,4 +74,8 @@ Global
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2C73BEEC-091B-45E4-A0BD-7D7CD16A8451}
EndGlobalSection
GlobalSection(SharedMSBuildProjectFiles) = preSolution
shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{3c8d945e-6074-437e-b6ea-237bd0c80411}*SharedItemsImports = 5
shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13
EndGlobalSection
EndGlobal
36 changes: 24 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,30 @@

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

## A Simple Algorithm for Boolean Operations on Polygons
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.

*Francisco Martínez, Carlos Ogayar, Juan R. Jiménez, Antonio J. Rueda*

https://sci-hub.se/10.1016/j.advengsoft.2013.04.004
## Features

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.
- Works with non-convex polygons, including holes and multiple disjoint regions
- Handles edge cases like overlapping edges and vertical segments
- Preserves topology: output polygons include hole/contour hierarchy
- Deterministic and robust sweep line algorithm with O((n + k) log n) complexity

The original code can be found in the reference folder.

The plan is to implement a performant port, add additional tests and some method by which to generate renders of output clipping operations.

This is currently an intellectual exercise but I believe a C# port could be very useful in many applications if proven successful.

All and any assistance is gratefully accepted. :heart:
## Usage

The API centers around `Polygon` and `Contour` types. Construct input polygons using contours, then apply Boolean operations via the `PolygonClipper` class:

```csharp
Polygon result = PolygonClipper.Intersect(subject, clipping);
```

## Based On

This implementation is based on the algorithm described in:

> F. Martínez et al., "A simple algorithm for Boolean operations on polygons", *Advances in Engineering Software*, 64 (2013), pp. 11–19.
> https://sci-hub.se/10.1016/j.advengsoft.2013.04.004

## License

Six Labors Split License. See [`LICENSE`](https://github.com/SixLabors/PolygonClipper/blob/main/LICENSE) for details.
98 changes: 56 additions & 42 deletions src/PolygonClipper/Contour.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Collections;
using System.Diagnostics;
using System.Runtime.CompilerServices;

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

/// <summary>
/// Set of points conforming the external contour
/// Set of vertices conforming the external contour
/// </summary>
private readonly List<Vertex> points = [];
private readonly List<Vertex> vertices = [];

/// <summary>
/// Holes of the contour. They are stored as the indexes of
/// the holes in a polygon class
/// </summary>
private readonly List<int> holes = [];
private readonly List<int> holeIndices = [];

/// <summary>
/// Gets the number of vertices.
/// </summary>
public int VertexCount => this.points.Count;

/// <summary>
/// Gets the number of edges.
/// </summary>
public int EdgeCount => this.points.Count;
public int Count
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.vertices.Count;
}

/// <summary>
/// Gets the number of holes.
/// </summary>
public int HoleCount => this.holes.Count;
public int HoleCount => this.holeIndices.Count;

/// <summary>
/// Gets a value indicating whether the contour is external (not a hole).
/// </summary>
public bool IsExternal => this.HoleOf == null;
public bool IsExternal => this.ParentIndex == null;

/// <summary>
/// Gets or sets the ID of the parent contour if this contour is a hole.
/// Gets or sets the index of the parent contour in the polygon if this contour is a hole.
/// </summary>
public int? HoleOf { get; set; }
public int? ParentIndex { get; set; }

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

/// <summary>
/// Gets the vertex at the specified index of the external contour.
/// Gets the vertex at the specified index.
/// </summary>
/// <param name="index">The index of the vertex.</param>
/// <returns>The <see cref="Vertex"/>.</returns>
public Vertex GetVertex(int index) => this.points[index];
/// <returns>The <see cref="Vertex"/> at the specified index.</returns>
public Vertex this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.vertices[index];
}

/// <summary>
/// Gets the hole index at the specified position in the contour.
/// </summary>
/// <param name="index">The index of the hole.</param>
/// <returns>The hole index.</returns>
public int GetHoleIndex(int index) => this.holes[index];
public int GetHoleIndex(int index) => this.holeIndices[index];

/// <summary>
/// Gets the segment at the specified index of the contour.
/// </summary>
/// <param name="index">The index of the segment.</param>
/// <returns>The <see cref="Segment"/>.</returns>
internal Segment Segment(int index)
=> (index == this.VertexCount - 1)
? new Segment(this.points[^1], this.points[0])
: new Segment(this.points[index], this.points[index + 1]);
internal Segment GetSegment(int index)
=> (index == this.Count - 1)
? new Segment(this.vertices[^1], this.vertices[0])
: new Segment(this.vertices[index], this.vertices[index + 1]);

/// <summary>
/// Gets the bounding box of the contour.
/// </summary>
/// <returns>The <see cref="Box2"/>.</returns>
public Box2 GetBoundingBox()
{
if (this.VertexCount == 0)
if (this.Count == 0)
{
return default;
}

List<Vertex> points = this.points;
List<Vertex> points = this.vertices;
Box2 b = new(points[0]);
for (int i = 1; i < points.Count; ++i)
{
Expand All @@ -109,18 +115,18 @@ public Box2 GetBoundingBox()
/// </returns>
public bool IsCounterClockwise()
{
if (this.precomputeCC)
if (this.hasCachedOrientation)
{
return this.cc;
return this.cachedCounterClockwise;
}

this.precomputeCC = true;
this.hasCachedOrientation = true;

double area = 0;
Vertex c;
Vertex c1;

List<Vertex> points = this.points;
List<Vertex> points = this.vertices;
for (int i = 0; i < points.Count - 1; i++)
{
c = points[i];
Expand All @@ -131,7 +137,7 @@ public bool IsCounterClockwise()
c = points[^1];
c1 = points[0];
area += Vertex.Cross(c, c1);
return this.cc = area >= 0;
return this.cachedCounterClockwise = area >= 0;
}

/// <summary>
Expand All @@ -147,8 +153,8 @@ public bool IsCounterClockwise()
/// </summary>
public void Reverse()
{
this.points.Reverse();
this.cc = !this.cc;
this.vertices.Reverse();
this.cachedCounterClockwise = !this.cachedCounterClockwise;
}

/// <summary>
Expand Down Expand Up @@ -180,7 +186,7 @@ public void SetCounterClockwise()
/// <param name="y">The y-coordinate offset.</param>
public void Translate(double x, double y)
{
List<Vertex> points = this.points;
List<Vertex> points = this.vertices;
for (int i = 0; i < points.Count; i++)
{
points[i] += new Vertex(x, y);
Expand All @@ -192,38 +198,46 @@ public void Translate(double x, double y)
/// </summary>
/// <param name="vertex">The vertex to add.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddVertex(in Vertex vertex) => this.points.Add(vertex);
public void AddVertex(in Vertex vertex) => this.vertices.Add(vertex);

/// <summary>
/// Removes the vertex at the specified index from the contour.
/// </summary>
/// <param name="index">The index of the vertex to remove.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveVertexAt(int index) => this.points.RemoveAt(index);
public void RemoveVertexAt(int index) => this.vertices.RemoveAt(index);

/// <summary>
/// Clears all vertices and holes from the contour.
/// </summary>
public void Clear()
{
this.points.Clear();
this.holes.Clear();
this.vertices.Clear();
this.holeIndices.Clear();
}

/// <summary>
/// Clears all holes from the contour.
/// </summary>
public void ClearHoles() => this.holes.Clear();
public void ClearHoles() => this.holeIndices.Clear();

/// <summary>
/// Gets the last vertex in the contour.
/// </summary>
/// <returns>The last <see cref="Vertex"/> in the contour.</returns>
public Vertex GetLastVertex() => this.points[^1];
public Vertex GetLastVertex() => this.vertices[^1];

/// <summary>
/// Adds a hole index to the contour.
/// </summary>
/// <param name="index">The index of the hole to add.</param>
public void AddHoleIndex(int index) => this.holes.Add(index);
public void AddHoleIndex(int index) => this.holeIndices.Add(index);

/// <inheritdoc/>
public IEnumerator<Vertex> GetEnumerator()
=> ((IEnumerable<Vertex>)this.vertices).GetEnumerator();

/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
=> ((IEnumerable)this.vertices).GetEnumerator();
}
Loading
Loading