Fix Bentley-Ottmann polygon walk at shared vertices#382
Conversation
When building result polygons, the next segment at a vertex was only searched in one direction around the snap-square event ring. Offset stroke outlines (e.g. dense adjacent cells) could leave no match and panic. Search both directions, fall back to another left endpoint in the result, and close the contour to the other endpoint instead of panicking when no neighbor exists. Add regression tests for merged grid stroke/settle. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Thank you for your contribution, but I can't get your test to panic like it says it should. In fact, we go from 500 path commands to 5 (MoveTo + 3 LineTo + Close) which is exactly right. Also when drawing it looks correct. The panic is actually there for a reason, it means something else went wrong in another part of the code. I still wonder if you could reproduce the original bug you encountered? |
Imports upstream PR tdewolff#382. The previous one-direction-only search at shared vertices panicked on dense geometry (e.g. stroked outlines clipped against rects whose edges align). Searches both directions around the snap-square event ring with a fallback to another left-endpoint at the vertex.
…the panic The previous tests used a manually-crafted exact unit-square grid, which does not trigger the bentleyOttmann polygon-walk panic on the pre-fix code because all vertices land exactly on the snap-rounding grid. The real reproduction requires floating-point imprecision: actual Voronoi cell boundaries (computed in github.com/aldernero/gaul) accumulate tiny errors (e.g. 1.4e-17 instead of 0.0) that create near-coincident vertices. When these vertices are in the stroke outline of 100 merged cells and settled with the Positive fill rule, the one-directional event-ring search fails to find the next result segment and panics: next node for result polygon is nil, probably buggy intersection code Add testdata/voronoi_stroke_pre_settle.gob: the gob-encoded pre-Settle stroke outline generated from that Voronoi geometry. TestSettleVoronoiDenseGridStrokeOutline loads it and calls Settle(Positive); it panics on the pre-fix code and passes with the fix applied. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
You are correct — the original tests I added used a manually-crafted exact unit-square grid, which does not reproduce the panic. I've pushed a fix. Why the original tests didn't panicThe bug requires floating-point imprecision in the vertex coordinates. With a manually-crafted grid all coordinates land exactly on the snap-rounding grid ( The actual reproduction comes from the Voronoi algorithm: even though the sites are on a regular grid (so the cells should be perfect axis-aligned squares), the floating-point arithmetic accumulates tiny errors — e.g. a vertex that should be exactly What's in the updated PRI replaced the four synthetic tests (which all passed on the pre-fix code) with:
The fixture was generated by: // sites on a 10×10 regular grid, VoronoiWithRect gives near-exact square cells
merged := paths.Merge()
canvas.FastStroke = true
preSettle := merged.Stroke(0.2, canvas.ButtCap, canvas.MiterJoin, 0.05)
// encode preSettle to testdata/voronoi_stroke_pre_settle.gobLet me know if you'd prefer a different approach for the fixture (e.g. inlining a minimal failing path as a string constant, or referencing the gaul test as the external reproducer). |
Summary
bentleyOttmannwhen building result polygons at vertices where many segments meet (e.g. afterPath.Stroke→Settleon dense, shared-edge geometry such as merged Voronoi cell outlines).path_intersection_voronoi_test.go).Problem
Path.StrokecallsSettle(Positive)on the offset outline. At tight junctions, polygon walking only searched one direction for the next segment at a vertex. When no match was found, the code panicked:next node for result polygon is nil, probably buggy intersection codeTest plan
go test ./...in this repoTestVoronoiCanvasMergedStroke_denseGrid)Made with Cursor