Repository navigation
Request: a release with the FixSelfIntersects fix (#1067). Difference loses most of its area in 2.0.0 and 2.0.1 (NonZero, C#) #1122
Description
Summary
Clipper.Difference with FillRule.NonZero, on a simple five-vertex subject and a simple triangle clip, returns 29,655,090.5 square units where the exact answer is 135,974,247. Almost 80% of the result is missing, including points well inside the subject and outside the clip. Commit 4da1564 on main ("Fixed a bug in ClipperBase::FixSelfIntersects (#1067)", 2026-02-22) fixes this input. No release contains that commit yet: NuGet's newest is 2.0.0 and the newest tag is 2.0.1. This issue asks for one, and offers the input below as a regression test.
Versions
| Version | Result |
|---|---|
NuGet Clipper2 2.0.0 (newest on NuGet as of 2026-10-23) |
fails as below |
Tag Clipper2_2.0.1 (21ebba0), C# library built from source |
fails, same numbers |
main at 20f05b4, the parent of 4da1564 |
fails, same numbers |
main at 4da1564, and at a6e7294 (2026-10-17) |
correct: area 135,927,331.5, within rounding of the exact area |
Input
Integer coordinates, one path each, both simple.
subject: (-503, 4889) (40000, 20000) (34522, 23202) (61000, 41700) (-4000, 3200)
clip: (63046, 38116) (0, 10000) (-556, 4862)
Expected and actual
| Area | |
|---|---|
| Exact NonZero difference, computed independently (signed ray-crossing winding, integrated exactly over horizontal slabs cut at every vertex and crossing) | 135,974,247.0 |
Clipper.Difference(subject, clip, FillRule.NonZero), 2.0.0 and 2.0.1 |
29,655,090.5 (three paths) |
The point (22716, 15000) is inside the subject and outside the clip, but outside every path of the result.
Reproduction (C#, NuGet Clipper2 2.0.0 only)
using Clipper2Lib;
// A simple five-vertex subject and a simple triangle clip.
var subject = new Paths64 { Clipper.MakePath(new long[]
{ -503, 4889, 40000, 20000, 34522, 23202, 61000, 41700, -4000, 3200 }) };
var clip = new Paths64 { Clipper.MakePath(new long[]
{ 63046, 38116, 0, 10000, -556, 4862 }) };
Paths64 difference = Clipper.Difference(subject, clip, FillRule.NonZero);
Console.WriteLine($"Clipper2 {typeof(Clipper).Assembly.GetName().Version}");
Console.WriteLine($"subject area: {Clipper.Area(subject)}");
Console.WriteLine($"clip area: {Clipper.Area(clip)}");
Console.WriteLine($"difference area: {Clipper.Area(difference)}");
// (22716, 15000) is inside the subject and outside the clip,
// so it belongs in the difference; the result leaves it out.
var probe = new Point64(22716, 15000);
Console.WriteLine($"probe vs subject: {Clipper.PointInPolygon(probe, subject[0])}");
Console.WriteLine($"probe vs clip: {Clipper.PointInPolygon(probe, clip[0])}");
bool inResult = false;
foreach (Path64 path in difference)
{
var where = Clipper.PointInPolygon(probe, path);
Console.WriteLine($"probe vs output: {where} (output path area {Clipper.Area(path)})");
if (where == PointInPolygonResult.IsInside) inResult = !inResult;
}
Console.WriteLine(inResult ? "ok" : "FAIL: the difference lost a point it should keep");
return inResult ? 0 : 1;Output with 2.0.0:
subject area: 200159500
clip area: 154148926
difference area: 29655090.5
probe vs subject: IsInside
probe vs clip: IsOutside
probe vs output: IsOutside (output path area 29058161)
probe vs output: IsOutside (output path area -59761)
probe vs output: IsOutside (output path area 656690.5)
FAIL: the difference lost a point it should keep
Notes
- 4da1564 removes the "adjacent intersections (ie a micro self-intersection)" shortcut in
FixSelfIntersects, so the cleanup goes throughDoSplitOp. On this input the old shortcut drops a large part of the outline. - The input was reduced automatically from a larger real case: 31 vertices in three paths, losing about 190,000 units in 1,150,000. The reduction drops a path or a vertex, rounds a coordinate, or moves or scales the whole input, and keeps each change only while the difference still loses area. It is a local minimum, not necessarily the smallest failing input.
- Checked against this repository on 2026-10-26:
Clipper2_2.0.1(21ebba0) is 7 commits behind 4da1564 and does not contain it.
Found by an automated soak test, and reduced and analysed with AI assistance; the repro and its numbers were run and checked as given above.
Activity