Conversation
When integer intersection points close a narrow crack between thin edges into a small hole, the output path before cleanup is still correct there: the remaining path winds +1 and a smaller split of opposite orientation winds -1. DoSplitOp discarded that split as a spurious twist, so the remaining path filled the hole and Union covered area outside all inputs. Before discarding such a split, check whether its centroid has a non-zero winding number relative to the remaining path; if it does, keep the split as a separate path. Splits outside the remaining path are still discarded. Same change in C++, C# and Delphi. Tests/Polygons.txt case 196: three thin triangles whose union previously covered (-7, 7).
|
Independent confirmation, with more cases. I ran into this while validating an exact boundary-winding area implementation against Clipper2 (PolylineKit, Every disagreement I found between Clipper2 and exact references is fixed by this PR:
"ok" means within 1e-6 of the reference; the PR's largest remaining difference on the integer cases is 6.9e-7, consistent with quantization at precision 8. References:
The integer inputs are listed in clipper-disagreements.json. The real pairs are not degenerate (no exact collinearity or shared vertices); their coordinates are derived from the $1 dataset, which I don't redistribute, but I can send them. Minimal case (7 vertices, single path). Vertex (2,1) lies exactly on the path's own edge (3,0)→(0,3):
Suggested regression test, which fails on |
Problem
Paths64 subject = { {{91,7},{-145,-11},{-141,-15}}, {{-28,75},{-33,76},{1,0}}, {{-22,51},{-39,76},{-25,-2}}, }; Paths64 solution = Union(subject, FillRule::NonZero);Point (−7, 7) is inside none of the three triangles (in exact arithmetic triangles 2 and 3 do not touch; there is a narrow crack between them), but it is inside the solution. The solution is one path:
Its area is 1 865, of which 649 lies outside the subject paths.
Cause
The intersections (0.95, 0.13) and (0.94, 0.13) become (0, 0). That moves edge (−33,76)→(0,0) across edge (−24,−1)→(−22,51) and closes the crack into a small hole. Before
CleanCollinearthe output path is still correct at (−7, 7):The main part winds +1 there, and the loop (0,0) (−24,−1) (−22,50) winds −1.
FixSelfIntersectscallsDoSplitOpon those two segments. The loop is smaller than the remaining path and of opposite orientation, so it takes the discard branch — the −1 is lost and the remaining path covers the hole.Discarding a reversed split that lies outside the remaining path only removes a sliver; discarding one that lies inside fills a hole.
Change
Before discarding a smaller split of opposite orientation, check whether its centroid (kept in doubles, so it is not rounded outside a thin triangle) has a non-zero winding number relative to the remaining path. If so, the split is kept as a separate path. Splits outside the remaining path are discarded as before.
Same change in C++, C# and Delphi (
TriangleInsidePathnext toAreaTriangle, one extra term in theDoSplitOpcondition). New case 196 inTests/Polygons.txt: the three triangles above, expected area 1 254 and 2 paths (the outer path and the hole(0,0) (-24,-1) (-22,50)).Verification
DoSplitOpis a line-by-line port and which returns the identical wrong path on this input:Paths64andPolyTree64; union time 89.2 → 91.9 ms;Polygons.txtand the rest of its suite pass.Related issues
Not the same as #1083 (a non-simple output path without extra area) or #1085 (a zero-width bridge; its input is unaffected by this change). #1067 also reported extra area with sliver triangles, but current C++ already returns the correct result for that input.