Skip to content

Commit

Permalink
Improve performance of Clip renamed as FastClip, improve performance …
Browse files Browse the repository at this point in the history
…of Rect.ContainsPoint
  • Loading branch information
tdewolff committed Jan 16, 2025
1 parent 90838f7 commit 91fc79e
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 67 deletions.
51 changes: 22 additions & 29 deletions path_simplify.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,8 @@ func (q heapVW) down(i0, n int) bool {
return i0 < i
}

// Clip removes all segments that are completely outside the given clipping rectangle. To ensure that the removal doesn't cause a segment to cross the rectangle from the outside, it keeps points that cross at least two lines to infinity along the rectangle's edges. This is much quicker (along O(n)) than using p.And(canvas.Rectangle(x1-x0, y1-y0).Translate(x0, y0)) (which is O(n log n)).
func (p *Path) Clip(x0, y0, x1, y1 float64) *Path {
// FastClip removes all segments that are completely outside the given clipping rectangle. To ensure that the removal doesn't cause a segment to cross the rectangle from the outside, it keeps points that cross at least two lines to infinity along the rectangle's edges. This is much quicker (along O(n)) than using p.And(canvas.Rectangle(x1-x0, y1-y0).Translate(x0, y0)) (which is O(n log n)).
func (p *Path) FastClip(x0, y0, x1, y1 float64) *Path {
if x1 < x0 {
x0, x1 = x1, x0
}
Expand All @@ -295,54 +295,45 @@ func (p *Path) Clip(x0, y0, x1, y1 float64) *Path {
// TODO: we could check if the path is only in two external regions (left/right and top/bottom)
// and if no segment crosses the rectangle, it is fully outside the rectangle

var rectSegs Rect // sum of rects of prev removed points
// Note that applying AND to multiple Cohen-Sutherland outcodes will give us whether all points are left/right and/or above/below
// the rectangle.
var first, start, prev Point
//crosses := false
outcodes := 0 // cumulative of removed segments
startOutcode := 0
pendingMoveTo := true
for i := 0; i < len(p.d); {
cmd := p.d[i]
i += cmdLen(cmd)

end := Point{p.d[i-3], p.d[i-2]}
if cmd == MoveToCmd {
rectSegs = Rect{end.X, end.Y, end.X, end.Y}
startOutcode = cohenSutherlandOutcode(rect, end, 0.0)
outcodes = startOutcode
pendingMoveTo = true
start = end
continue
}

rectSeg := RectFromPoints(start, end)
endOutcode := cohenSutherlandOutcode(rect, end, 0.0)
outcodes &= endOutcode
switch cmd {
//case LineToCmd, CloseCmd:
//if !crosses && rect.Touches(rectSeg) {
// crosses = true
//}
case QuadToCmd:
rectSeg = rectSeg.AddPoint(Point{p.d[i-5], p.d[i-4]})
//if !crosses && rect.Touches(rectSeg) {
// crosses = true
//}
outcodes &= cohenSutherlandOutcode(rect, Point{p.d[i-5], p.d[i-4]}, 0.0)
case CubeToCmd:
rectSeg = rectSeg.AddPoint(Point{p.d[i-7], p.d[i-6]})
rectSeg = rectSeg.AddPoint(Point{p.d[i-5], p.d[i-4]})
//if !crosses && rect.Touches(rectSeg) {
// crosses = true
//}
outcodes &= cohenSutherlandOutcode(rect, Point{p.d[i-7], p.d[i-6]}, 0.0)
outcodes &= cohenSutherlandOutcode(rect, Point{p.d[i-5], p.d[i-4]}, 0.0)
case ArcToCmd:
rx, ry, phi := p.d[i-7], p.d[i-6], p.d[i-5]
large, sweep := toArcFlags(p.d[i-4])
cx, cy, _, _ := ellipseToCenter(start.X, start.Y, rx, ry, phi, large, sweep, end.X, end.Y)
rectSeg = rectSeg.AddPoint(Point{cx - rx, cy - ry})
rectSeg = rectSeg.AddPoint(Point{cx + rx, cy + ry})
//if !crosses && rect.Touches(rectSeg) {
// crosses = true
//}
outcodes &= cohenSutherlandOutcode(rect, Point{cx - rx, cy - ry}, 0.0)
outcodes &= cohenSutherlandOutcode(rect, Point{cx + rx, cy + ry}, 0.0)
}

rectSegs = rectSegs.Add(rectSeg)
if cmd == CloseCmd {
// either start is inside, or entire segment is left/right or above/below
if crosses := outcodes == 0; cmd == CloseCmd {
if !pendingMoveTo {
if rect.Touches(rectSegs) && start != prev {
if crosses && start != prev {
// previous segments were skipped
q.d = append(q.d, LineToCmd, start.X, start.Y, LineToCmd)
}
Expand All @@ -353,18 +344,20 @@ func (p *Path) Clip(x0, y0, x1, y1 float64) *Path {
q.d = append(q.d, CloseCmd, first.X, first.Y, CloseCmd)
pendingMoveTo = true
}
} else if rect.Touches(rectSegs) {
} else if crosses {
if pendingMoveTo {
q.d = append(q.d, MoveToCmd, start.X, start.Y, MoveToCmd)
pendingMoveTo = false
first = start
} else if start != prev {
// previous segments were skipped
q.d = append(q.d, LineToCmd, start.X, start.Y, LineToCmd)
}
q.d = append(q.d, p.d[i-cmdLen(cmd):i]...)
rectSegs = Rect{end.X, end.Y, end.X, end.Y}
outcodes = endOutcode
prev = end
}
startOutcode = endOutcode
start = end
}
return q
Expand Down
4 changes: 2 additions & 2 deletions path_simplify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func TestPathSimplifyVisvalingamWhyatt(t *testing.T) {
}), p)
}

func TestPathClip(t *testing.T) {
func TestPathFastClip(t *testing.T) {
tests := []struct {
p string
rect Rect
Expand All @@ -71,7 +71,7 @@ func TestPathClip(t *testing.T) {
t.Run(tt.p, func(t *testing.T) {
p := MustParseSVGPath(tt.p)
r := MustParseSVGPath(tt.r)
test.T(t, p.Clip(tt.rect.X0, tt.rect.Y0, tt.rect.X1, tt.rect.Y1), r)
test.T(t, p.FastClip(tt.rect.X0, tt.rect.Y0, tt.rect.X1, tt.rect.Y1), r)
})
}
}
71 changes: 35 additions & 36 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,19 +464,12 @@ func (r Rect) Expand(d float64) Rect {
return r
}

// ContainsPoint returns true if the rectangles contains a point, not if it touches an edge.
// ContainsPoint returns true if the rectangle contains or touches an edge.
func (r Rect) ContainsPoint(p Point) bool {
if p.X < r.X0 || r.X1 < p.X {
// left or right
return false
} else if p.Y < r.Y0 || r.Y1 < p.Y {
// below or above
return false
}
return true
return cohenSutherlandOutcode(r, p, 0.0) == 0
}

// TouchesPoint returns true if the rectangles contains or touches a point.
// TouchesPoint returns true if the rectangle touches a point (within +-Epsilon).
func (r Rect) TouchesPoint(p Point) bool {
return Interval(p.X, r.X0, r.X1) && Interval(p.Y, r.Y0, r.Y1)
}
Expand Down Expand Up @@ -548,11 +541,13 @@ func (r Rect) ContainsLine(a, b Point) bool {
}

func (r Rect) OverlapsLine(a, b Point) bool {
return cohenSutherlandLineClip(r, a, b, 0.0)
overlaps, _ := cohenSutherlandLineClip(r, a, b, 0.0)
return overlaps
}

func (r Rect) TouchesLine(a, b Point) bool {
return cohenSutherlandLineClip(r, a, b, Epsilon)
_, touches := cohenSutherlandLineClip(r, a, b, Epsilon)
return touches
}

// Contains returns true if r contains q.
Expand Down Expand Up @@ -854,31 +849,35 @@ func (m Matrix) ToSVG(h float64) string {

////////////////////////////////////////////////////////////////

func cohenSutherlandLineClip(rect Rect, a, b Point, eps float64) bool {
computeOutcode := func(p Point) int {
code := 0x0000
if p.X < rect.X0-eps {
code |= 0x0001 // left
} else if rect.X1+eps < p.X {
code |= 0x0010 // right
}
if p.Y < rect.Y0-eps {
code |= 0x0100 // bottom
} else if rect.Y1+eps < p.Y {
code |= 0x1000 // top
}
return code
func cohenSutherlandOutcode(rect Rect, p Point, eps float64) int {
code := 0b0000
if p.X < rect.X0-eps {
code |= 0b0001 // left
} else if rect.X1+eps < p.X {
code |= 0b0010 // right
}
if p.Y < rect.Y0-eps {
code |= 0b0100 // bottom
} else if rect.Y1+eps < p.Y {
code |= 0b1000 // top
}
return code
}

outcode0 := computeOutcode(a)
outcode1 := computeOutcode(b)
// return whether line is (partially) inside the rectangle, and whether it is partially inside the rectangle.
func cohenSutherlandLineClip(rect Rect, a, b Point, eps float64) (bool, bool) {
outcode0 := cohenSutherlandOutcode(rect, a, eps)
outcode1 := cohenSutherlandOutcode(rect, b, eps)
if outcode0 == 0 && outcode1 == 0 {
return true, false
}
for {
if (outcode0 | outcode1) == 0 {
// both inside
return true
return true, true
} else if (outcode0 & outcode1) != 0 {
// both in same region outside
return false
return false, false
}

// pick point outside
Expand All @@ -889,30 +888,30 @@ func cohenSutherlandLineClip(rect Rect, a, b Point, eps float64) bool {

// intersect with rectangle
var c Point
if (outcodeOut & 0x1000) != 0 {
if (outcodeOut & 0b1000) != 0 {
// above
c.X = a.X + (b.X-a.X)*(rect.Y1-a.Y)/(b.Y-a.Y)
c.Y = rect.Y1
} else if (outcodeOut & 0x0100) != 0 {
} else if (outcodeOut & 0b0100) != 0 {
// below
c.X = a.X + (b.X-a.X)*(rect.Y0-a.Y)/(b.Y-a.Y)
c.Y = rect.Y0
} else if (outcodeOut & 0x0010) != 0 {
} else if (outcodeOut & 0b0010) != 0 {
// right
c.X = rect.X1
c.Y = a.Y + (b.Y-a.Y)*(rect.X1-a.X)/(b.X-a.X)
} else if (outcodeOut & 0x0001) != 0 {
} else if (outcodeOut & 0b0001) != 0 {
// left
c.X = rect.X0
c.Y = a.Y + (b.Y-a.Y)*(rect.X0-a.X)/(b.X-a.X)
}

// prepare next pass
if outcodeOut == outcode0 {
outcode0 = computeOutcode(c)
outcode0 = cohenSutherlandOutcode(rect, c, eps)
a = c
} else {
outcode1 = computeOutcode(c)
outcode1 = cohenSutherlandOutcode(rect, c, eps)
b = c
}
}
Expand Down

0 comments on commit 91fc79e

Please sign in to comment.