Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 41 additions & 20 deletions path_intersection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions path_intersection_voronoi_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading