Klip 3.1.0
See the version list below for details.
dotnet add package Klip --version 3.1.0
NuGet\Install-Package Klip -Version 3.1.0
<PackageReference Include="Klip" Version="3.1.0" />
<PackageVersion Include="Klip" Version="3.1.0" />
<PackageReference Include="Klip" />
paket add Klip --version 3.1.0
#r "nuget: Klip, 3.1.0"
#:package Klip@3.1.0
#addin nuget:?package=Klip&version=3.1.0
#tool nuget:?package=Klip&version=3.1.0

Klip
An F# library for fast and robust polygon clipping.
Klip is a partial port of Clipper2 covering the general polygon boolean operations - intersection, union, difference, and XOR. Offsetting, rectangle-only clipping, and triangulation are not included.
It runs on .NET and JavaScript via Fable, so the same source serves Rhino, Revit, and browser apps. To make it suitable for JS runtimes it is in many parts derived from the TypeScript port clipper2-ts. All original tests pass, along with many new tests for unions of almost-aligned polygons.
The key difference from Clipper2: Klip uses float coordinates throughout instead of int64.
Clipper2 snaps every coordinate onto an integer grid before clipping; Klip removes that step and computes
directly on the unrounded input. Intersection points are kept at full floating-point precision rather than
snapped to the grid, so the exact input positions are preserved.
Coordinate precision
Because the engine computes on unrounded float coordinates, point coincidence, colinearity, and
horizontality use small per-instance tolerances instead of exact equality. The defaults absorb
floating-point noise without fusing genuinely distinct points:
- Point coincidence - two coordinates are the same point when they differ by less than a small absolute distance.
- Colinearity - three points are colinear when the turn angle between their edges falls below a small scale-free angle tolerance.
- Horizontality - an edge is horizontal when its slope falls below a small scale-free angle
tolerance, rather than an exact
topY = botYtest. This keeps a shared near-horizontal edge that is a hair off exact (e.g. a top at37vs37.000001) from landing its ends on distinct scanlines and sealing an open notch into a phantom hole. - Adjacent-edge joins - the near-top guard scales with local edge height, and the perpendicular join distance defaults to the point-coincidence tolerance.
Contours that share a seam are merged: horizontal seams join when their X-ranges overlap (a real seam's overlap far exceeds float noise), and sloped/near-vertical seams join via the tolerance-gated adjacent-edge checks. Contours that touch at a single point (e.g. the two lobes of an XOR) remain separate, as in Clipper2.
You do not need to scale coordinates before clipping - use your source units directly. If results are off
for your coordinate magnitude (e.g. seam-sharing pieces come out separate, or slivers survive), the
tolerances don't fit your inputs: set them all from one absolute tolerance with the Tolerance
property (see Tolerances and scaling) rather than rescaling your input.
The Snap module can optionally pre-snap almost-aligned coordinates (see below).
Types
Original documentation: https://www.angusj.com/clipper2
Types keep their original C# names. The ..64 suffix historically meant 64-bit integers; in Klip the XY
coordinates are float, and there is no separate ..D API because the regular path types already preserve
floating-point coordinates.
Path64<'Z>: a single contour. X and Y are stored in a flat interleavedResizeArray<float>asx0, y0, x1, y1, ....Paths64<'Z>: aResizeArray<Path64<'Z>>- multiple contours, such as an outer polygon and its holes.PolyTree64<'Z>: a tree output that preserves parent-child contour relationships (holes inside outers).ZCallback64<'Z>: a callback assigning user-defined'Zmetadata to vertices created at intersections.
Generic 'Z metadata
'Z is an optional generic type parameter for user-defined metadata attached to vertices, defaulting to
unit. (In the original Clipper2 the optional Z value is always int64.) 'Z values are metadata, not
a 3rd coordinate.
If you do not use 'Z, use the no-Z helpers such as Path64.createFrom and Paths64.createSingle, which
produce Path64<unit> / Paths64<unit> values. The 'Z-aware helpers live in the parallel ...Z
functions and the KlipperZ module.
Path helpers
The Path64 and Paths64 modules provide construction and utility helpers:
createFrom,createFromSeq(on both modules) copy coordinate data into new buffers.createDirectlyreuses the suppliedResizeArraybuffers directly (coordinates are not rounded).createFromXYMembers/createFromxyMembersaccept objects withX/Yorx/ymembers.enableZ/enableZWithattach metadata buffers and reject paths that already have Z values.mapXY,iterXY,mapZ,iterZ, orientation helpers, andsignedAreacover common inspection and transformation tasks.
Boolean operations
Closed-polygon wrappers
The Klipper.* wrappers always treat input as closed polygons:
intersect clip subject- intersection of subject and clip.union clip subject- union of subject and clip.unionSelf subject- resolves self-intersections within a single subject.unionSelfChecked subject- reorients all subjects to positive orientation before unioning.difference clip subject- regions of subject not inside clip.xor clip subject- regions in subject or clip but not both.removeSelfIntersectionsPositive subject/removeSelfIntersectionsNegative subject- resolve one self-intersecting path using the matching directional fill rule.
For a custom ClipType, FillRule, or PolyTree64 output:
booleanOp (clipType, subject, clip, fillRule)- returnsPaths64<unit>.booleanOpPolyTree (clipType, subject, clip, fillRule)- returns aPolyTree64<unit>preserving the parent-child hierarchy.polyTreeToPaths64 polyTree- flattens aPolyTree64<unit>back intoPaths64<unit>.
Each function has a counterpart in the KlipperZ module that takes an option<ZCallback64<'Z>> (first
argument for the wrappers, trailing zCallback argument for booleanOp / booleanOpPolyTree) to attach
'Z metadata.
open Klip
let subject =
Paths64.createSingle [ 0.0; 0.0; 10.0; 0.0; 10.0; 10.0; 0.0; 10.0 ]
let clip =
Paths64.createSingle [ 5.0; 5.0; 15.0; 5.0; 15.0; 15.0; 5.0; 15.0 ]
let union = Klipper.union clip subject
let intersection = Klipper.intersect clip subject
let nonZeroDifference =
Klipper.booleanOp (ClipType.Difference, subject, clip, FillRule.NonZero)
Open vs closed paths
Open/closed is not inferred from coordinates (a trailing vertex equal to the first is just stripped) - each path is tagged when added to the engine. Rules, inherited from Clipper2:
- Subject paths can be open or closed; clip paths are always closed.
- For
Intersection,Difference, andXor: open and closed subjects are processed independently - closed subjects are ignored for the open-path solution, and vice versa. - For
Union: open subjects are clipped wherever they overlap any closed path (subject or clip).
The Klipper.* and KlipperZ.* wrappers always treat input as closed. To clip open paths
(polylines / line segments), use Clipper64 directly and call AddOpenSubject:
let c = Clipper64<unit>()
c.AddOpenSubject(openLines) // polylines - endpoints stay endpoints
c.AddSubject(closedPolygons) // optional, closed
c.AddClip(clipPolygons) // clip is always closed
// Execute returns a (closedSolution, openSolution) tuple;
// openSolution is null when no open subjects were added.
let closedSolution, openSolution = c.Execute(ClipType.Intersection, FillRule.EvenOdd)
Calling AddPaths with PathType.Clip and isOpen = true is invalid. ExecutePolyTree follows the same
open-output convention as Execute.
Direct Clipper64 options
Use Clipper64<'Z> directly for open subjects, repeated execution with the same input, or lower-level
tuning:
PreserveColinear: keep removable colinear vertices in closed solutions.Tolerance: sets all five scale-dependent tolerances from one absolute tolerance - the distance below which points are considered identical and lines touching. The value is used as-is, not as a multiplier of the defaults; see Tolerances and scaling.ReverseSolution: reverses output orientation.ZCallback: computes metadata for vertices created at intersections.
The individual tolerance properties (point coincidence, adjacent-edge joins, colinearity, horizontality,
the near-top join guard, and the sliver culls) remain functional as expert overrides but are marked
[<Obsolete>] and hidden from editor completion - the Tolerance property is the supported tuning surface.
Each is documented in detail on the member itself in Src/Engine.fs.
Tolerances and scaling
The distance tolerances are absolute and do not auto-scale - the engine does not normalize coordinate
magnitude. Set them all from one absolute tolerance with c.Tolerance <- t - the distance below which
points are considered identical and lines touching: the four distance tolerances become t, and the area-valued split
tolerance becomes t² (valid range 0.0 .. 1e12; 0 makes the comparisons exact). The value is used
as-is, not as a multiplier of the defaults - those are calibrated for integer-Clipper2-style inputs of
coordinate magnitude ~1e6 and do not correspond to any single call of this method.
Every tolerance comparison in the engine is dimensionally homogeneous, so clipping is scale-equivariant:
scaling all inputs by s together with the tolerance yields the identically scaled solution.
The angle tolerances are scale-independent and never need adjusting.
Snap preprocessing
Optionally call Snap.xAndY tolerance pathGroups or Snap.xAndYSingle tolerance paths to snap nearly-equal
x and y coordinates to their respective averages. This is an in-place mutation done before adding paths to
Clipper64. Call it on all paths at once so the same shared coordinate is used across subject and clip.
Building
For .NET:
dotnet build
dotnet test Test/FSharp/Tests/Tests1/Tests1.fsproj
dotnet test Test/FSharp/Tests/Tests2/TestsZ.fsproj
For JavaScript:
cd Test/TypeScript
dotnet tool restore
npm install
npm run clean # clean previous Fable output
npm run build # F# → JavaScript via Fable, then vite build
npm test # vitest --run, against the compiled bundle
npm run buildts # optional: F# → TypeScript via Fable, then tsc and vite build
cd ../..
The JavaScript bundle ends up in Test/TypeScript/_dist/Klip.mjs and is what the Vitest suite imports - rebuild
before testing after any F# source change. The TypeScript/Fable build emits a separate bundle under
Test/TypeScript/_distTS/Klip.mjs.
Performance
On .NET, the local benchmark harness is roughly on par with Clipper2 C#. In JavaScript, the latest local
run is about the same as clipper2-ts and about 80% slower than clipper2-wasm on average.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- FSharp.Core (>= 6.0.7)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
### Fixed
- Tolerance-horizontality is now capped by `CoordEqTolerance`: an edge classifies horizontal only when `abs Δy <= min(HorizontalAngleTolerance * abs Δx, CoordEqTolerance)`, i.e. the angle is shallow enough AND the endpoint Ys are point-coincident. Previously a long shallow edge could classify horizontal with an endpoint-Y difference far above point coincidence; `doHorizontal` then consumed it in one scanbeam, a beam before its far-end scanline, stranding another contour's seam edge arriving there - the touching contours stayed separate instead of merging (2 output paths with correct total area). The regime was reachable whenever a seam's two sides straddled the horizontality threshold (their slope ratios differ by the edge-length ratio) with the shifted vertex below the flat run, e.g. `UnionTouchingBridgeSweepProducesSingleElevenPointPolygon` at seam slopes in `(1e-5, 2e-5]` after the `HorizontalAngleTolerance` default raise - and existed before the raise too, just 10x narrower. With the cap, such edges sweep as sloped and merge via the normal crossing/join machinery; edges with float-noise Y-spans (the phantom-hole case the tolerance exists for) still classify horizontal. The cap also makes the classification consistent with the horizontal-segment machinery, whose output-point run walks always used point coincidence on Y. `Eng.isHorizontalCoords`/`getDx`/`setDx`/`trimHorz` and `ActiveEdge.create` now take `coordEqTol` as an explicit argument, following the existing tolerance-passing convention.
### Changed
- `Engine`: ported the sweep-loop optimizations from [clipper2-ts#34](https://github.com/countertype/clipper2-ts/pull/34) - skip the intersection merge sort on scanbeams where the active edge list is already sorted by `curX` (no intersections possible), and reuse the edge positions computed during that scan in `doTopOfScanbeam`; `isHorizontal` now reads the already-maintained `ae.dx` (±infinity if horizontal, see `Eng.getDx`) instead of re-testing the angle from bot/top coordinates; `checkJoinLeft`/`checkJoinRight` check the cheap `curX` mismatch before the hot/horizontal/open edge state; `Eng.boundingBoxesOverlap` early-exits on the first separating axis instead of computing all eight min/max values up front; `convertHorzSegsToJoins` compacts valid horizontal segments to the front of the list before sorting instead of sorting (and re-testing) invalid ones too; `addPathsToVertexList` now stores only each path's head vertex in `vertexList` (the rest of the chain stays reachable through the `next`/`prev` links), cutting one array slot per vertex. No change in clipping results - verified against the full F# (`Tests1`, `Tests2`) and JS (`vitest`) suites. The JS benchmark suite (`Test/bench/clipping-operations.bench.ts`) shows most intersection/difference/xor/union cases 10-40% faster, in line with the upstream PR's reported gains. Added `Test/FSharp/Benchmark/VitestFixtureBenchmarks.cs`, a BenchmarkDotNet suite comparing Klip against the Clipper2 2.0.0 NuGet package on those same fixture shapes (rather than `Benchmarks.cs`'s dense random-polygon dataset): on complex-polygon and geo-scale cases Klip is within ~1-6% of Clipper2 either way (noise level), but ~12-25% slower on grid/many-small-paths cases (`Union_MediumGrid`, `Union_LargeGrid`, `Intersection_Grid`, `Union_SimpleRects`/`TwoOverlapping`/`FourCircles`) - a divergence not visible in `Benchmarks.cs`'s single-large-path dataset, worth investigating separately.
### Added
- The `Clipper64.AngleTolerance` get/set property sets both dimensionless angle tolerances from a single angle in **degrees**: the colinearity tolerance becomes `sin θ` and the horizontal-angle tolerance `sin θ / 100` (horizontality is kept 100x tighter because classifying an edge as horizontal changes how the scanbeam processes it). The getter returns the current colinearity turn angle in degrees (default ~0.057°, i.e. `sin θ = 1e-3`). The `HorizontalAngleTolerance` default is raised from `1e-6` to `1e-5` so the defaults already follow this 100x coupling. Valid range `0.0` .. `5.7` degrees; `0.0` makes both comparisons exact, and the upper bound keeps the derived horizontal tolerance within its safe `1e-3` limit. Being scale-free it needs no rescaling to the input's coordinate magnitude, unlike `Tolerance`.
- The `Clipper64.Tolerance` get/set property sets all five scale-dependent tolerances from a single absolute tolerance - the distance below which points are considered identical and lines touching: `CoordEqTolerance` = `MergeVertexTolerance` = `NearTopYToleranceCap` = `SmallTriangleTolerance` = the given tolerance, and the area-valued `SplitAreaTolerance` = the tolerance squared. The value is used as-is (valid range `0.0` .. `1e12`; `0` makes all five comparisons exact), **not** as a multiplier of the engine defaults - those (`1e-5` for the point tolerances, `2.0` for the culls) are calibrated for integer-Clipper2-style inputs of coordinate magnitude ~1e6 and do not correspond to any single value of this property. The dimensionless tolerances (`ColinearityTolerance`, `HorizontalAngleTolerance`, `NearTopYToleranceFactor`) are scale-invariant and untouched. Every tolerance comparison in the engine is dimensionally homogeneous, so clipping is scale-equivariant: scaling all input coordinates by `s` together with the tolerance yields the identically scaled solution - bit-exact for power-of-two `s`, verified by the new `ToleranceUnitTests`. The individual tolerance properties (`CoordEqTolerance`, `MergeVertexTolerance`, `NearTopYToleranceCap`, `SmallTriangleTolerance`, `SplitAreaTolerance`, and the dimensionless `ColinearityTolerance`, `HorizontalAngleTolerance`, `NearTopYToleranceFactor`) remain functional as expert overrides (e.g. raising `MergeVertexTolerance` above the seam gap of noisy inputs, or zeroing the sliver culls) but are now marked `[<Obsolete>]` to hide them from editor completion - the `Tolerance` property is the supported tuning surface; their valid ranges widen to `1e12` for the distances and `1e24` for `SplitAreaTolerance` to match.