Skip to content

Commit

Permalink
Add Path.LineClip and fix bug for Path.FastClip
Browse files Browse the repository at this point in the history
  • Loading branch information
tdewolff committed Jan 20, 2025
1 parent 3d81a7d commit 84f9a27
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 16 deletions.
82 changes: 74 additions & 8 deletions path_simplify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
36 changes: 35 additions & 1 deletion path_simplify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
14 changes: 7 additions & 7 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 84f9a27

Please # to comment.