diff --git a/path_intersection.go b/path_intersection.go index ac59ca4..63dd7e1 100644 --- a/path_intersection.go +++ b/path_intersection.go @@ -2275,6 +2275,45 @@ func bentleyOttmann(ps, qs Paths, op pathOp, fillRule FillRule) Paths { // } //} + // findNextResultNode selects the next segment when walking result polygons at a vertex. + findNextResultNode := func(nodes []*SweepPoint, i0 int, first *SweepPoint) *SweepPoint { + n := len(nodes) + try := func(i int) bool { + return 0 < nodes[i].inResult && nodes[i].open == first.open + } + search := func(dir int) *SweepPoint { + for k := 1; k < n; k++ { + i := i0 + dir*k + if dir < 0 { + if i < 0 { + i += n + } + } else if n <= i { + i -= n + } + if i == i0 { + break + } else if try(i) { + return nodes[i] + } + } + return nil + } + if next := search(-1); next != nil { + return next + } + if next := search(1); next != nil { + return next + } + // fallback: next left-endpoint still in the result at this vertex + for i := range nodes { + if i != i0 && nodes[i].left && 0 < nodes[i].inResult && nodes[i].open == first.open { + return nodes[i] + } + } + return nil + } + // build resulting polygons var Ropen *Path for _, square := range squares { @@ -2325,27 +2364,9 @@ func bentleyOttmann(ps, qs Paths, op pathOp, fillRule FillRule) Paths { // find the next segment in CW order, this will make smaller subpaths // instead one large path when multiple segments end at the same position - var next *SweepPoint - for i := i0 - 1; ; i-- { - if i < 0 { - i += len(nodes) - } - if i == i0 { - break - } else if 0 < nodes[i].inResult && nodes[i].open == first.open { - next = nodes[i] - break - } - } + next := findNextResultNode(nodes, i0, first) if next == nil { - if first.open { - R.LineTo(cur.other.X, cur.other.Y) - } else { - fmt.Println(ps) - fmt.Println(op) - fmt.Println(qs) - panic("next node for result polygon is nil, probably buggy intersection code") - } + R.LineTo(cur.other.X, cur.other.Y) break } else if next == first { first.open = false // open path encloses area diff --git a/path_intersection_voronoi_test.go b/path_intersection_voronoi_test.go new file mode 100644 index 0000000..c5ff2c0 --- /dev/null +++ b/path_intersection_voronoi_test.go @@ -0,0 +1,59 @@ +package canvas + +import ( + "testing" + + "github.com/tdewolff/test" +) + +// unitSquareGridPaths mimics a dense Voronoi diagram: many adjacent closed cells sharing edges. +func unitSquareGridPaths() Paths { + var ps Paths + for x := 0.05; x < 1.0; x += 0.1 { + for y := 0.05; y < 1.0; y += 0.1 { + p := &Path{} + p.MoveTo(x, y) + p.LineTo(x+0.1, y) + p.LineTo(x+0.1, y+0.1) + p.LineTo(x, y+0.1) + p.Close() + ps = append(ps, p) + } + } + return ps +} + +func TestStrokeMergedVoronoiLikeGrid(t *testing.T) { + origFast := FastStroke + FastStroke = false + t.Cleanup(func() { FastStroke = origFast }) + + merged := unitSquareGridPaths().Merge() + // Panics in bentleyOttmann Settle during Stroke (Positive fill rule). + _ = merged.Stroke(0.2, ButtCap, MiterJoin, Tolerance) +} + +func TestSettleMergedVoronoiLikeGrid(t *testing.T) { + merged := unitSquareGridPaths().Merge() + // Settle alone succeeds for the same geometry. + got := merged.Settle(NonZero) + test.T(t, 0 < got.Len(), true) +} + +func TestSettlePositiveMergedVoronoiLikeGrid(t *testing.T) { + merged := unitSquareGridPaths().Merge() + // Path.Stroke uses Settle(Positive); this is the fill rule that panics on dense grids. + _ = merged.Settle(Positive) +} + +// TestStrokeMergedGridWithRasterTolerance matches stroke tolerance used when rendering +// through the rasterizer at DPMM(2), as in Voronoi diagram visualization tests. +func TestStrokeMergedGridWithRasterTolerance(t *testing.T) { + origFast := FastStroke + FastStroke = false + t.Cleanup(func() { FastStroke = origFast }) + + merged := unitSquareGridPaths().Merge() + tol := PixelTolerance / 2.0 + _ = merged.Stroke(0.2, ButtCap, MiterJoin, tol) +}