Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
c3ca1de
Avoid extra work and normalize
JimBobSquarePants Feb 5, 2026
89542a0
Update PolygonClipper.cs
JimBobSquarePants Feb 6, 2026
60a418c
SelfIntersectionRemover
JimBobSquarePants Feb 6, 2026
c7c2b9d
Update SelfIntersectionRemover.cs
JimBobSquarePants Feb 6, 2026
128a637
Optimize
JimBobSquarePants Feb 6, 2026
e139925
Support collection init for Contour
JimBobSquarePants Feb 6, 2026
5631373
Status save.. Nearly works.
JimBobSquarePants Feb 7, 2026
73e3cac
FIx tests
JimBobSquarePants Feb 7, 2026
17d2191
Update SelfIntersectionRemover.cs
JimBobSquarePants Feb 7, 2026
192e2e6
Much closer.
JimBobSquarePants Feb 7, 2026
cc4c130
Use hierarchy
JimBobSquarePants Feb 7, 2026
4b34766
Cleanup
JimBobSquarePants Feb 7, 2026
b89393e
Add comments
JimBobSquarePants Feb 7, 2026
33df722
Update PolygonClipper.cs
JimBobSquarePants Feb 7, 2026
cde693c
Update tests.
JimBobSquarePants Feb 7, 2026
81d81aa
Add failing test
JimBobSquarePants Feb 7, 2026
e7e9565
Fix failing tests
JimBobSquarePants Feb 8, 2026
419cc36
Update SelfIntersectionRemover.cs
JimBobSquarePants Feb 8, 2026
54aa643
Document everything
JimBobSquarePants Feb 8, 2026
33a169d
Add more tests.
JimBobSquarePants Feb 8, 2026
0cfd9dd
Merge touching contours
JimBobSquarePants Feb 8, 2026
10b7625
Use initial capacity
JimBobSquarePants Feb 8, 2026
ad29f21
All initializing with capacity
JimBobSquarePants Feb 8, 2026
10d4f73
Add benches
JimBobSquarePants Feb 8, 2026
249b7f7
Optimize
JimBobSquarePants Feb 8, 2026
dc0bc05
Update Benches.cs
JimBobSquarePants Feb 8, 2026
2753f45
Optimize split sorting and half-edge graph
JimBobSquarePants Feb 8, 2026
f85aafa
Use SegmentSplit struct for segment splits
JimBobSquarePants Feb 8, 2026
9092463
Refactor contour merging and add bounds utilities
JimBobSquarePants Feb 8, 2026
c1c8695
Update tests
JimBobSquarePants Feb 8, 2026
62c8c04
Update readme
JimBobSquarePants Feb 8, 2026
2dfa45f
Merge loop
JimBobSquarePants Feb 8, 2026
61546d3
Begin Viatti style intersection port
JimBobSquarePants Feb 9, 2026
2b9e2b9
Cleanup
JimBobSquarePants Feb 9, 2026
3442f91
Further refactoring and cleanup
JimBobSquarePants Feb 9, 2026
d5bd970
Further cleanup.
JimBobSquarePants Feb 9, 2026
8947ef5
Use Vertex more.
JimBobSquarePants Feb 9, 2026
a9fcd80
Update PolygonUtilities.cs
JimBobSquarePants Feb 9, 2026
32425ad
Inline input builder.
JimBobSquarePants Feb 9, 2026
8879812
Add more docs, remove unused
JimBobSquarePants Feb 9, 2026
6a5b9a1
Refactor and remove unused
JimBobSquarePants Feb 9, 2026
a2399d0
Update SelfIntersectionUnionClipper.cs
JimBobSquarePants Feb 9, 2026
8a3e3cc
Simplify vertex counts
JimBobSquarePants Feb 9, 2026
4048eeb
Update SelfIntersectionRemover.cs
JimBobSquarePants Feb 9, 2026
f1b18f8
Do not flip y
JimBobSquarePants Feb 9, 2026
990e9af
Remove IsOpen code
JimBobSquarePants Feb 9, 2026
a114532
Rename and document
JimBobSquarePants Feb 9, 2026
e0f70da
Remove clip windcount tracking
JimBobSquarePants Feb 9, 2026
99d7770
Rename and document type
JimBobSquarePants Feb 9, 2026
ada7a1a
More docs and cleanup
JimBobSquarePants Feb 9, 2026
260e9e2
More cleanup and docs
JimBobSquarePants Feb 9, 2026
a9468c6
Document and rename methods
JimBobSquarePants Feb 10, 2026
08f0a6e
Further inline comment cleanup
JimBobSquarePants Feb 10, 2026
1039f33
Use Vertex.Abs
JimBobSquarePants Feb 10, 2026
73ae3da
Begin remove IsAlmostZero
JimBobSquarePants Feb 10, 2026
6f86ad0
Revert some zero checks. Better Vertex methods
JimBobSquarePants Feb 10, 2026
4622a37
Extract into more OOP pattern
JimBobSquarePants Feb 10, 2026
7357a49
More refactoring
JimBobSquarePants Feb 10, 2026
f57ff31
Add OrderedSet
JimBobSquarePants Feb 10, 2026
0c57aa6
Teak comparison checks and do better benchmarks
JimBobSquarePants Feb 11, 2026
8f18163
Fixed draft
JimBobSquarePants Feb 11, 2026
be04844
Restore double precision Martinez
JimBobSquarePants Feb 12, 2026
507d33f
minor cleanup
JimBobSquarePants Feb 12, 2026
dd99627
All tests pass.
JimBobSquarePants Feb 12, 2026
c242ac3
Update PolygonClipper.Tests.csproj
JimBobSquarePants Feb 12, 2026
d6b20d4
Rent buffers.
JimBobSquarePants Feb 12, 2026
e182273
Code compiles, test work and benchmarks fast
JimBobSquarePants Feb 12, 2026
7b65105
Add additional fill rules
JimBobSquarePants Feb 12, 2026
739fe38
Update docs and inline casts
JimBobSquarePants Feb 12, 2026
244ccc9
Normalize closed contours.
JimBobSquarePants Feb 13, 2026
95efd66
Use overload.
JimBobSquarePants Feb 13, 2026
221e92f
Add 3rd party notices
JimBobSquarePants Feb 13, 2026
b6702e4
Remove referenced projects
JimBobSquarePants Feb 13, 2026
5fc709e
Update README.md
JimBobSquarePants Feb 13, 2026
96d70e4
All tests pass
JimBobSquarePants Feb 13, 2026
64fbbb5
Add stroking and cleanup.
JimBobSquarePants Feb 16, 2026
932e6b2
Cleanup
JimBobSquarePants Feb 16, 2026
78bcaa9
Update Contour semantics
JimBobSquarePants Feb 16, 2026
3c54c9b
Optimize statusline
JimBobSquarePants Feb 16, 2026
aeb12e3
Implement pooling.
JimBobSquarePants Feb 17, 2026
8da8f61
Stroke normalization should be optional
JimBobSquarePants Feb 17, 2026
3142c01
Make methods pure
JimBobSquarePants Feb 18, 2026
8e2d171
Fix joining
JimBobSquarePants Feb 18, 2026
a94117a
No need for fill rule!!
JimBobSquarePants Feb 18, 2026
873832a
Update README.md
JimBobSquarePants Feb 18, 2026
118f528
Fix stroker docs.
JimBobSquarePants Feb 18, 2026
5f2716f
Update PolygonStroker.cs
JimBobSquarePants Feb 18, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
branches:
- main
- release/*
types: [ labeled, opened, synchronize, reopened ]
types: [ opened, synchronize, reopened ]
jobs:
# Prime a single LFS cache and expose the exact key for the matrix
WarmLFS:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/code-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ jobs:
XUNIT_PATH: .\tests\PolygonClipper.Tests # Required for xunit

- name: Codecov Update
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors')
with:
flags: unittests
token: ${{ secrets.CODECOV_TOKEN }}
7 changes: 5 additions & 2 deletions 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
# Visual Studio Version 18
VisualStudioVersion = 18.2.11415.280
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PolygonClipper", "src\PolygonClipper\PolygonClipper.csproj", "{3C8D945E-6074-437E-B6EA-237BD0C80411}"
EndProject
Expand All @@ -16,11 +16,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
.gitattributes = .gitattributes
.gitignore = .gitignore
reference\10.1016@j.advengsoft.2013.04.004.pdf = reference\10.1016@j.advengsoft.2013.04.004.pdf
reference\129902.129906.pdf = reference\129902.129906.pdf
ci-build.ps1 = ci-build.ps1
ci-pack.ps1 = ci-pack.ps1
ci-test.ps1 = ci-test.ps1
LICENSE = LICENSE
README.md = README.md
THIRD-PARTY-NOTICES.md = THIRD-PARTY-NOTICES.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeoJson", "tests\GeoJson\GeoJson.csproj", "{F881441F-D3B6-4B48-8CEF-8DC0746D4578}"
Expand Down
63 changes: 55 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,81 @@ SixLabors.PolygonClipper
<div align="center">

[![Build Status](https://img.shields.io/github/actions/workflow/status/SixLabors/PolygonClipper/build-and-test.yml?branch=main)](https://github.com/SixLabors/PolygonClipper/actions)
[![Code coverage](https://codecov.io/gh/SixLabors/PolygonClipper/branch/main/graph/badge.svg)](https://codecov.io/gh/SixLabors/PolygonClipper)
[![codecov](https://codecov.io/github/SixLabors/PolygonClipper/graph/badge.svg?token=ZEK38fv18V)](https://codecov.io/github/SixLabors/PolygonClipper)
[![License: Six Labors Split](https://img.shields.io/badge/license-Six%20Labors%20Split-%23e30183)](https://github.com/SixLabors/PolygonClipper/blob/main/LICENSE)

</div>

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.
SixLabors.PolygonClipper provides high-performance polygon clipping and stroking in C#.
Boolean operations (union, intersection, difference, xor) are implemented with a Martínez-Rueda sweep-line pipeline for complex polygons with holes and multiple contours.
Contour normalization is handled by a dedicated Vatti/Clipper2-inspired pipeline (`PolygonClipper.Normalize`) that resolves self-intersections/overlaps into positive-winding output.
`PolygonStroker` can optionally run that normalization pass on emitted stroke geometry.

## Features

- 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
- Includes `PolygonClipper.Normalize` (Clipper2-inspired) for positive-winding contour normalization
- Includes `PolygonStroker` for configurable geometric stroking (joins, caps, miter limits)
- Uses double precision geometry without coordinate quantization

## Usage

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

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

## Based On
Boolean operations can process self-intersecting inputs directly.
Use normalization when you want canonical positive-winding contours (for example, before export or rendering pipelines that rely on winding semantics):

This implementation is based on the algorithm described in:
```csharp
Polygon clean = PolygonClipper.Normalize(input);
```

`Normalize` uses a fixed positive-winding normalization path.

### Stroking

Use `PolygonStroker` to generate filled stroke polygons from input contours:

```csharp
Polygon stroked = PolygonStroker.Stroke(input, width: 12);
```

Configure join/cap behavior through `StrokeOptions`:

```csharp
StrokeOptions options = new()
{
LineJoin = LineJoin.Round,
LineCap = LineCap.Round,
MiterLimit = 4,
InnerMiterLimit = 1.01,
ArcDetailScale = 1
};

Polygon stroked = PolygonStroker.Stroke(input, width: 12, options);
```

## Algorithm References

This project draws on the following algorithm and implementation references:

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

> B. R. Vatti, "A generic solution to polygon clipping", *Communications of the ACM*, 35(7), 1992, pp. 56-63.
> https://dl.acm.org/doi/pdf/10.1145/129902.129906

> 21re, *rust-geo-booleanop* (Rust Martínez-Rueda implementation reference).
> https://github.com/21re/rust-geo-booleanop

> 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
> Angus Johnson, *Clipper2* polygon clipping library (reference implementation for the normalization pipeline).
> https://github.com/AngusJohnson/Clipper2

## License

Expand Down
67 changes: 67 additions & 0 deletions THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Third-Party Notices

This project includes code and algorithmic adaptations derived from the following third-party projects.

## Clipper2

- Project: `Clipper2`
- Upstream: <https://github.com/AngusJohnson/Clipper2>
- Referenced commit: `20f05b475ea81e60a230b92e4d33438cd39dd8e1`
- License: `Boost Software License - Version 1.0`
- Upstream copyright notice:
- `Copyright : Angus Johnson 2010-2025`

### Boost Software License - Version 1.0 - August 17th, 2003

Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:

The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

## rust-geo-booleanop

- Project: `rust-geo-booleanop`
- Upstream: <https://github.com/21re/rust-geo-booleanop>
- Referenced commit: `b9bbdbb34ebfa2aaeb99665842e64ede1696ed65`
- License: `MIT License`
- Upstream copyright notice:
- `Copyright (c) 2018 Alexander Milevski`

### MIT License

Copyright (c) 2018 Alexander Milevski

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Binary file added reference/129902.129906.pdf
Binary file not shown.
194 changes: 194 additions & 0 deletions src/PolygonClipper/ActiveEdge.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Runtime.CompilerServices;

namespace SixLabors.PolygonClipper;

/// <summary>
/// Represents an edge that is currently active in the sweep-line.
/// </summary>
/// <remarks>
/// The sweep assumes a Y-axis-positive-down coordinate system. "Bottom" and "Top"
/// refer to the lower and upper scanline endpoints (larger and smaller Y respectively).
/// </remarks>
internal sealed class ActiveEdge
{
#pragma warning disable SA1401 // Hot sweep state uses fields to avoid accessor overhead.
/// <summary>
/// The lower endpoint of the edge in scanline order.
/// </summary>
public Vertex Bottom;

/// <summary>
/// The upper endpoint of the edge in scanline order.
/// </summary>
public Vertex Top;

/// <summary>
/// The X coordinate where the edge intersects the current scanline.
/// </summary>
public double CurrentX;

/// <summary>
/// The delta-X per delta-Y for the edge (its scanline slope).
/// </summary>
public double Dx;

/// <summary>
/// The winding delta contributed by this edge (+1 or -1).
/// </summary>
public int WindDelta;

/// <summary>
/// The accumulated winding count for this edge.
/// </summary>
public int WindCount;

/// <summary>
/// The output record this edge is contributing to, if any.
/// </summary>
public OutputRecord? OutputRecord;

/// <summary>
/// The previous edge in the Active Edge List (AEL).
/// </summary>
public ActiveEdge? PrevInAel;

/// <summary>
/// The next edge in the Active Edge List (AEL).
/// </summary>
public ActiveEdge? NextInAel;

/// <summary>
/// The previous edge in the Sorted Edge List (SEL).
/// </summary>
public ActiveEdge? PrevInSel;

/// <summary>
/// The next edge in the Sorted Edge List (SEL).
/// </summary>
public ActiveEdge? NextInSel;

/// <summary>
/// The temporary link used when sorting intersections.
/// </summary>
public ActiveEdge? Jump;

/// <summary>
/// The current top vertex for this edge's bound.
/// </summary>
public SweepVertex? VertexTop;

/// <summary>
/// The local minima that spawned this edge.
/// </summary>
public LocalMinima LocalMin;

/// <summary>
/// Indicates whether this edge is the left bound of its pair.
/// </summary>
public bool IsLeftBound;

/// <summary>
/// The pending join state for this edge.
/// </summary>
public JoinWith JoinWith;
#pragma warning restore SA1401

/// <summary>
/// Gets a value indicating whether this edge currently contributes to output.
/// </summary>
public bool IsHot => this.OutputRecord != null;

/// <summary>
/// Gets a value indicating whether the edge is horizontal within tolerance.
/// </summary>
public bool IsHorizontal => this.Top.Y == this.Bottom.Y;

/// <summary>
/// Gets a value indicating whether a horizontal edge is heading right.
/// </summary>
public bool IsHeadingRightHorizontal => double.IsNegativeInfinity(this.Dx);

/// <summary>
/// Gets a value indicating whether a horizontal edge is heading left.
/// </summary>
public bool IsHeadingLeftHorizontal => double.IsPositiveInfinity(this.Dx);

/// <summary>
/// Gets a value indicating whether the current top vertex is a local maxima.
/// </summary>
public bool IsMaxima => this.VertexTop != null && this.VertexTop.IsMaxima;

/// <summary>
/// Gets a value indicating whether this edge is the front edge of its output record.
/// </summary>
public bool IsFront => this.OutputRecord != null && this == this.OutputRecord.FrontEdge;

/// <summary>
/// Gets the next input vertex along the bound in the winding direction.
/// </summary>
public SweepVertex NextVertex => this.WindDelta > 0 ? this.VertexTop!.Next! : this.VertexTop!.Prev!;

/// <summary>
/// Gets the vertex two steps behind the current top, used for turn tests.
/// </summary>
public SweepVertex PrevPrevVertex => this.WindDelta > 0 ? this.VertexTop!.Prev!.Prev! : this.VertexTop!.Next!.Next!;

/// <summary>
/// Finds the previous hot edge in the AEL, if any.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ActiveEdge? GetPrevHotEdge()
{
ActiveEdge? prev = this.PrevInAel;
while (prev != null && !prev.IsHot)
{
prev = prev.PrevInAel;
}

return prev;
}

/// <summary>
/// Calculates the X coordinate where this edge intersects the scanline at <paramref name="currentY" />.
/// </summary>
// This method sits on the hottest path in large self-intersection workloads.
// AggressiveOptimization consistently improves codegen here versus tiered defaults.
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static double TopX(ActiveEdge edge, double currentY)
{
if (currentY == edge.Top.Y || edge.Top.X == edge.Bottom.X)
{
return edge.Top.X;
}

if (currentY == edge.Bottom.Y)
{
return edge.Bottom.X;
}

return edge.Bottom.X + (edge.Dx * (currentY - edge.Bottom.Y));
}

/// <summary>
/// Recomputes <see cref="Dx" /> from the current endpoints.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void UpdateDx() => this.Dx = GetDx(this.Bottom, this.Top);

/// <summary>
/// Computes delta-X per delta-Y, returning infinities for horizontal edges.
/// </summary>
private static double GetDx(Vertex pt1, Vertex pt2)
{
double dy = pt2.Y - pt1.Y;
if (dy != 0)
{
return (pt2.X - pt1.X) / dy;
}

return pt2.X > pt1.X ? double.NegativeInfinity : double.PositiveInfinity;
}
}
Loading
Loading