From 91fc79e71919738251308ff9ae8cb5a01efff6c2 Mon Sep 17 00:00:00 2001 From: Taco de Wolff Date: Thu, 16 Jan 2025 20:02:10 +0100 Subject: [PATCH] Improve performance of Clip renamed as FastClip, improve performance of Rect.ContainsPoint --- path_simplify.go | 51 ++++++++++++++----------------- path_simplify_test.go | 4 +-- util.go | 71 +++++++++++++++++++++---------------------- 3 files changed, 59 insertions(+), 67 deletions(-) diff --git a/path_simplify.go b/path_simplify.go index 3715303..5dfbd53 100644 --- a/path_simplify.go +++ b/path_simplify.go @@ -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 } @@ -295,9 +295,11 @@ 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] @@ -305,44 +307,33 @@ func (p *Path) Clip(x0, y0, x1, y1 float64) *Path { 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) } @@ -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 diff --git a/path_simplify_test.go b/path_simplify_test.go index 5688ee5..e8b6391 100644 --- a/path_simplify_test.go +++ b/path_simplify_test.go @@ -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 @@ -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) }) } } diff --git a/util.go b/util.go index 822baeb..cf0ae19 100644 --- a/util.go +++ b/util.go @@ -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) } @@ -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. @@ -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 @@ -889,19 +888,19 @@ 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) @@ -909,10 +908,10 @@ func cohenSutherlandLineClip(rect Rect, a, b Point, eps float64) bool { // 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 } }