Skip to content

Commit

Permalink
Remove Path.Decimate, add VisvalingamWhyatt filtered variant
Browse files Browse the repository at this point in the history
  • Loading branch information
tdewolff committed Jan 2, 2025
1 parent 9a4bc2c commit ee12333
Showing 1 changed file with 13 additions and 120 deletions.
133 changes: 13 additions & 120 deletions path_simplify.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,16 @@ func (item simplifyItemVW) String() string {
}

func (p *Path) SimplifyVisvalingamWhyatt(tolerance float64) *Path {
return p.SimplifyVisvalingamWhyattFilter(tolerance, nil)
}

func (p *Path) SimplifyVisvalingamWhyattFilter(tolerance float64, filter func(Point) bool) *Path {
area := func(a, b, c Point) float64 {
return 0.5 * math.Abs(a.PerpDot(b)+b.PerpDot(c)+c.PerpDot(a))
}

q := &Path{} //d: p.d[:0]} // reuse memory
// don't reuse memory since the new path may be much smaller and keep the extra capacity
q := &Path{}
pq := NewPriorityQueue[int](nil, 0)
for _, pi := range p.Split() {
if len(pi.d) <= 4 || len(pi.d) <= 4+cmdLen(pi.d[4]) {
Expand All @@ -81,7 +86,7 @@ func (p *Path) SimplifyVisvalingamWhyatt(tolerance float64) *Path {
idx := len(list)
j := i + cmdLen(pi.d[i])
next := Point{pi.d[j-3], pi.d[j-2]}
if 4 < i || closed {
if (4 < i || closed) && (filter == nil || filter(cur)) {
A = area(prev, cur, next)
pq.Append(idx)
}
Expand Down Expand Up @@ -111,7 +116,9 @@ func (p *Path) SimplifyVisvalingamWhyatt(tolerance float64) *Path {
for 0 < pq.Len() {
idx := pq.Pop()
cur := list[idx]
if tolerance <= cur.area {
if math.IsNaN(cur.area) {
continue
} else if tolerance <= cur.area {
break
}

Expand Down Expand Up @@ -142,8 +149,7 @@ func (p *Path) SimplifyVisvalingamWhyatt(tolerance float64) *Path {
}

q.d = append(q.d, MoveToCmd, list[first].X, list[first].Y, MoveToCmd)
for idx := first; idx < list[idx].next; {
idx = list[idx].next
for idx := list[first].next; idx != -1 && idx != first; idx = list[idx].next {
q.d = append(q.d, LineToCmd, list[idx].X, list[idx].Y, LineToCmd)
}
if closed {
Expand All @@ -154,117 +160,6 @@ func (p *Path) SimplifyVisvalingamWhyatt(tolerance float64) *Path {
return q
}

// Decimate decimates the path using the Visvalingam-Whyatt algorithm. Assuming path is flat and has no subpaths.
func (p *Path) Decimate(tolerance float64) *Path {
var j int // j is index until written from p
q := &Path{d: p.d} // reuse memory
write := func(i int) {
if 0 < j {
if j < i {
q.d = append(q.d, p.d[j:i]...)
}
} else {
// first write
q.d = q.d[:i]
}
j = i
}
remove := func(i int) {
write(i)
j += cmdLen(p.d[i])
}
area := func(a, b, c Point) float64 {
return 0.5 * math.Abs(a.PerpDot(b)+b.PerpDot(c)+c.PerpDot(a))
}

var i0 int // length of previous subpaths
for _, pi := range p.Split() {
var i, n int
var prev, cur Point
closed := pi.Closed()
if len(pi.d) <= 4 || len(pi.d) <= 4+cmdLen(pi.d[4]) {
// must have at least 3 commands
write(i0)
j += len(pi.d)
i0 += len(pi.d)
continue
} else if closed {
prev = Point{pi.d[len(pi.d)-7], pi.d[len(pi.d)-6]}
cur = Point{pi.d[1], pi.d[2]}
i, n = 0, len(pi.d)-4
} else {
prev = Point{pi.d[1], pi.d[2]}
cur = Point{pi.d[5], pi.d[6]}
i, n = 4, len(pi.d)-cmdLen(pi.d[len(pi.d)-1])
}

start := i0
if 0 < j {
start = len(q.d)
}
removed := false
for i < n {
iNext := i + cmdLen(pi.d[i])
next := Point{pi.d[iNext+cmdLen(pi.d[iNext])-3], pi.d[iNext+cmdLen(pi.d[iNext])-2]}

//fmt.Println(prev, cur, next, "--", area(prev, cur, next))
if area(prev, cur, next) < tolerance {
// remove point
remove(i0 + i)
cur = next
removed = true
} else {
if removed {
// move back and check again
for start < len(q.d) && start+cmdLen(q.d[start]) < len(q.d) {
m := cmdLen(q.d[len(q.d)-1])
cur = prev
prev = Point{q.d[len(q.d)-m-3], q.d[len(q.d)-m-2]}
if area(prev, cur, next) < tolerance {
q.d = q.d[:len(q.d)-m]
} else {
break
}
}
removed = false
}
prev = cur
cur = next
}
i = iNext
}

end := i0 + len(pi.d)
if 0 < j {
// write rest of subpath
write(i0 + len(pi.d))
end = len(q.d)
}
if start+cmdLen(q.d[start]) < end && q.d[start+cmdLen(q.d[start])] != CloseCmd {
// set MoveTo
if m := cmdLen(q.d[start]); m != 4 {
copy(q.d[start:], q.d[start+m-4:])
q.d = q.d[:len(q.d)-m+4]
}
q.d[start] = MoveToCmd
q.d[start+3] = MoveToCmd

if closed {
// update Close command
q.d[end-3] = q.d[start+1]
q.d[end-2] = q.d[start+2]
}
} else {
// remove small paths
q.d = q.d[:start]
j = i0 + len(pi.d)
}
i0 += len(pi.d)
}
write(i0)
return q
}

// 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 {
if x1 < x0 {
Expand All @@ -275,11 +170,12 @@ func (p *Path) Clip(x0, y0, x1, y1 float64) *Path {
}
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{}
startIn := false
pendingMoveTo := true
first, start := Point{}, Point{}
var moveToIndex, firstInIndex int
q := &Path{} //d: p.d[:0]} // q is always smaller or equal to p
for i := 0; i < len(p.d); {
cmd := p.d[i]
i += cmdLen(cmd)
Expand Down Expand Up @@ -425,8 +321,5 @@ func (p *Path) Clip(x0, y0, x1, y1 float64) *Path {
start = end
startIn = endIn
}
if p.Closed() && !q.Empty() && !q.Closed() {
fmt.Println("WARNING: clip result not closed")
}
return q
}

0 comments on commit ee12333

Please sign in to comment.