From 84f9a27d4b5fb6ab80f5f45a1ee7256e2aae7ca7 Mon Sep 17 00:00:00 2001 From: Taco de Wolff Date: Mon, 20 Jan 2025 22:33:41 +0100 Subject: [PATCH] Add Path.LineClip and fix bug for Path.FastClip --- path_simplify.go | 82 ++++++++++++++++++++++++++++++++++++++----- path_simplify_test.go | 36 ++++++++++++++++++- util.go | 14 ++++---- 3 files changed, 116 insertions(+), 16 deletions(-) diff --git a/path_simplify.go b/path_simplify.go index 401aded..9fcf44d 100644 --- a/path_simplify.go +++ b/path_simplify.go @@ -294,7 +294,8 @@ func (q heapVW) down(i0, n int) bool { } // 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 { +func (p *Path) FastClip(x0, y0, x1, y1 float64, closed bool) *Path { + // TODO: check if path is closed while processing instead of as parameter if x1 < x0 { x0, x1 = x1, x0 } @@ -309,15 +310,12 @@ func (p *Path) FastClip(x0, y0, x1, y1 float64) *Path { return q } - // 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 - // 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 - outcodes := 0 // cumulative of removed segments - startOutcode := 0 - pendingMoveTo := true + var pendingMoveTo bool + var startOutcode int + var outcodes int // cumulative of removed segments for i := 0; i < len(p.d); { cmd := p.d[i] i += cmdLen(cmd) @@ -326,8 +324,8 @@ func (p *Path) FastClip(x0, y0, x1, y1 float64) *Path { if cmd == MoveToCmd { startOutcode = cohenSutherlandOutcode(rect, end, 0.0) outcodes = startOutcode - pendingMoveTo = true start = end + pendingMoveTo = true continue } @@ -373,9 +371,77 @@ func (p *Path) FastClip(x0, y0, x1, y1 float64) *Path { q.d = append(q.d, p.d[i-cmdLen(cmd):i]...) outcodes = endOutcode prev = end + } else if !closed && pendingMoveTo { + // there is no line from previous point that may move inside + outcodes = endOutcode } startOutcode = endOutcode start = end } return q } + +// LineClip converts the path to line segments between all coordinates and clips those lines against the given rectangle. +func (p *Path) LineClip(x0, y0, x1, y1 float64) *Path { + if x1 < x0 { + x0, x1 = x1, x0 + } + if y1 < y0 { + y0, y1 = y1, y0 + } + rect := Rect{x0, y0, x1, y1} + + // don't reuse memory since the new path may be much smaller and keep the extra capacity + q := &Path{} + if len(p.d) <= 4 { + return q + } + + var in bool + var firstMoveTo, lastMoveTo int + var start Point + 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 { + start = end + continue + } + + a, b, inEntirely, inPartially := cohenSutherlandLineClip(rect, start, end, 0.0) + if inEntirely || inPartially { + if !in { + lastMoveTo = len(q.d) + q.d = append(q.d, MoveToCmd, a.X, a.Y, MoveToCmd) + in = true + } + q.d = append(q.d, LineToCmd, b.X, b.Y, LineToCmd) + } else { + in = false + } + if cmd == CloseCmd { + if in && firstMoveTo < lastMoveTo { + // connect the last segment with the first + if end := len(q.d) - lastMoveTo; end < lastMoveTo-firstMoveTo-4 { + tmp := make([]float64, end) + copy(tmp, q.d[lastMoveTo:]) + copy(q.d[firstMoveTo+end:], q.d[firstMoveTo+4:lastMoveTo]) + copy(q.d[firstMoveTo:], tmp) + } else { + tmp := make([]float64, lastMoveTo-firstMoveTo-4) + copy(tmp, q.d[firstMoveTo+4:lastMoveTo]) + copy(q.d[firstMoveTo:], q.d[lastMoveTo:]) + copy(q.d[firstMoveTo+end:], tmp) + } + q.d = q.d[:len(q.d)-4] + } + firstMoveTo = len(q.d) + lastMoveTo = firstMoveTo + in = false + } + start = end + } + return q +} diff --git a/path_simplify_test.go b/path_simplify_test.go index 95a7234..2ecd7fb 100644 --- a/path_simplify_test.go +++ b/path_simplify_test.go @@ -65,13 +65,47 @@ func TestPathFastClip(t *testing.T) { {"M5 14L14 5L14 14z", Rect{0, 0, 10, 10}, "M5 14L14 5L14 14z"}, //{"M16 5L16 16L5 16z", Rect{0, 0, 10, 10}, ""}, {"M-10 -10L20 -10L20 20L-10 20z", Rect{0, 0, 10, 10}, "M20 -10L20 20L-10 20L-10 -10z"}, + {"M9 11L11 11", Rect{0, 0, 10, 10}, ""}, + {"M15 5L15 15L5 15", Rect{0, 0, 10, 10}, ""}, } for _, tt := range tests { t.Run(tt.p, func(t *testing.T) { p := MustParseSVGPath(tt.p) r := MustParseSVGPath(tt.r) - test.T(t, p.FastClip(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, p.Closed()), r) + }) + } +} + +func TestPathLineClip(t *testing.T) { + tests := []struct { + p string + rect Rect + r string + }{ + {"M-5 5L5 5L5 15L-5 15z", Rect{0, 0, 10, 10}, "M0 5L5 5L5 10"}, + {"M1 5L9 5L9 15L1 15z", Rect{0, 0, 10, 10}, "M1 10L1 5L9 5L9 10"}, + {"M1 5L9 5L9 15L5 20L1 15z", Rect{0, 0, 10, 10}, "M1 10L1 5L9 5L9 10"}, + {"M-5 5L9 5L9 15L5 20L-5 15z", Rect{0, 0, 10, 10}, "M0 5L9 5L9 10"}, + {"M-5 15L-5 5L9 5L9 15L5 20z", Rect{0, 0, 10, 10}, "M0 5L9 5L9 10"}, + {"M20 2L30 2L30 8L20 8z", Rect{0, 0, 10, 10}, ""}, + {"M20 5L30 5L30 15L20 15z", Rect{0, 0, 10, 10}, ""}, + {"M20 -10L30 -10L30 20L20 20z", Rect{0, 0, 10, 10}, ""}, + {"M14 5L14 14L5 14z", Rect{0, 0, 10, 10}, "M9 10L10 9"}, + {"M14 14L5 14L14 5z", Rect{0, 0, 10, 10}, "M9 10L10 9"}, + {"M5 14L14 5L14 14z", Rect{0, 0, 10, 10}, "M9 10L10 9"}, + //{"M16 5L16 16L5 16z", Rect{0, 0, 10, 10}, ""}, + {"M-10 -10L20 -10L20 20L-10 20z", Rect{0, 0, 10, 10}, ""}, + {"M9 11L11 11", Rect{0, 0, 10, 10}, ""}, + {"M15 5L15 15L5 15", Rect{0, 0, 10, 10}, ""}, + } + + for _, tt := range tests { + t.Run(tt.p, func(t *testing.T) { + p := MustParseSVGPath(tt.p) + r := MustParseSVGPath(tt.r) + test.T(t, p.LineClip(tt.rect.X0, tt.rect.Y0, tt.rect.X1, tt.rect.Y1), r) }) } } diff --git a/util.go b/util.go index aaa4462..ebfd67c 100644 --- a/util.go +++ b/util.go @@ -546,12 +546,12 @@ func (r Rect) ContainsLine(a, b Point) bool { } func (r Rect) OverlapsLine(a, b Point) bool { - overlaps, _ := cohenSutherlandLineClip(r, a, b, 0.0) + _, _, overlaps, _ := cohenSutherlandLineClip(r, a, b, 0.0) return overlaps } func (r Rect) TouchesLine(a, b Point) bool { - _, touches := cohenSutherlandLineClip(r, a, b, Epsilon) + _, _, _, touches := cohenSutherlandLineClip(r, a, b, Epsilon) return touches } @@ -864,20 +864,20 @@ func cohenSutherlandOutcode(rect Rect, p Point, eps float64) int { return code } -// 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) { +// return whether line is inside the rectangle, either entirely or partially. +func cohenSutherlandLineClip(rect Rect, a, b Point, eps float64) (Point, Point, bool, bool) { outcode0 := cohenSutherlandOutcode(rect, a, eps) outcode1 := cohenSutherlandOutcode(rect, b, eps) if outcode0 == 0 && outcode1 == 0 { - return true, false + return a, b, true, false } for { if (outcode0 | outcode1) == 0 { // both inside - return true, true + return a, b, true, true } else if (outcode0 & outcode1) != 0 { // both in same region outside - return false, false + return a, b, false, false } // pick point outside