Skip to content

Commit

Permalink
Refactor SimplifyVisvalingamWhyatt, 19% faster
Browse files Browse the repository at this point in the history
  • Loading branch information
tdewolff committed Jan 7, 2025
1 parent 46c3e18 commit 61aad66
Showing 1 changed file with 142 additions and 42 deletions.
184 changes: 142 additions & 42 deletions path_simplify.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,34 @@ func (p *Path) Gridsnap(spacing float64) *Path {
}

type itemVW struct {
Point
pathIdx int32
heapIdx int32
area float64
prev, next int32 // indices into points
prev, next int32 // indices into items
}

func (item itemVW) Point(p *Path) Point {
return Point{p.d[item.pathIdx-3], p.d[item.pathIdx-2]}
}

func (item itemVW) String() string {
return fmt.Sprintf("%v %v (%v→·→%v)", item.Point, item.area, item.prev, item.next)
return fmt.Sprintf("%v %v (%v→·→%v)", item.pathIdx, item.area, item.prev, item.next)
}

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

func (p *Path) SimplifyVisvalingamWhyattFilter(tolerance float64, filter func(Point) bool) *Path {
tolerance *= 2.0 // save on 0.5 multiply in computeArea
computeArea := func(a, b, c Point) float64 {
return 0.5 * math.Abs(a.PerpDot(b)+b.PerpDot(c)+c.PerpDot(a))
return math.Abs(a.PerpDot(b) + b.PerpDot(c) + c.PerpDot(a))
}

// don't reuse memory since the new path may be much smaller and keep the extra capacity
q := &Path{}
pq := NewPriorityQueue[int32](nil, 0)
var heap heapVW
var items []itemVW
SubpathLoop:
for _, pi := range p.Split() {
closed := pi.Closed()
Expand All @@ -81,21 +88,19 @@ SubpathLoop:
if closed {
length--
}
items := make([]itemVW, 0, length)
pq.Reset(func(i, j int32) bool {
return items[i].area < items[j].area
}, length)
if cap(items) < length {
items = make([]itemVW, 0, length)
} else {
items = items[:0]
}
heap.Reset(length)

first := int32(0)
for i := 4; i < len(pi.d); {
j := i + cmdLen(pi.d[i])
next := Point{pi.d[j-3], pi.d[j-2]}

idx := int32(len(items))
area := math.NaN()
if (4 < i || closed) && (filter == nil || filter(cur)) {
area = computeArea(prev, cur, next)
pq.Append(idx)
}

idxPrev, idxNext := idx-1, idx+1
if closed {
if i == 4 {
Expand All @@ -104,31 +109,41 @@ SubpathLoop:
idxNext = 0
}
}

area := math.NaN()
add := (4 < i || closed) && (filter == nil || filter(cur))
if add {
area = computeArea(prev, cur, next)
}
items = append(items, itemVW{
Point: cur,
area: area,
prev: idxPrev,
next: idxNext,
pathIdx: int32(i),
area: area,
prev: idxPrev,
next: idxNext,
})
if add {
heap.Append(&items[idx])
} else if closed && first == idx {
first++
}

prev = cur
cur = next
i = j
}
if !closed {
items = append(items, itemVW{
Point: cur,
area: math.NaN(),
prev: int32(len(items) - 1),
next: -1,
pathIdx: int32(len(pi.d)),
area: math.NaN(),
prev: int32(len(items) - 1),
next: -1,
})
}

pq.Init()
heap.Init()

first := int32(0)
for 0 < pq.Len() {
idx := pq.Pop()
item := items[idx]
for 0 < len(heap) {
item := heap.Pop()
if tolerance <= item.area {
break
} else if item.prev == item.next {
Expand All @@ -139,38 +154,123 @@ SubpathLoop:
// remove current point from linked list, this invalidates those items in the queue
items[item.prev].next = item.next
items[item.next].prev = item.prev
if first == idx {
if item == &items[first] {
first = item.next
}

// update previous point
if prev := items[item.prev]; prev.prev != -1 && !math.IsNaN(prev.area) {
area := computeArea(items[prev.prev].Point, prev.Point, items[prev.next].Point)
items[item.prev].area = area
idx, _ := pq.Find(item.prev)
pq.Fix(idx)
if prev := &items[item.prev]; prev.prev != -1 && !math.IsNaN(prev.area) {
area := computeArea(items[prev.prev].Point(pi), prev.Point(pi), items[prev.next].Point(pi))
prev.area = area
heap.Fix(int(prev.heapIdx))
}

// update next point
if next := items[item.next]; next.next != -1 && !math.IsNaN(next.area) {
area := computeArea(items[next.prev].Point, next.Point, items[next.next].Point)
items[item.next].area = area
idx, _ := pq.Find(item.next)
pq.Fix(idx)
if next := &items[item.next]; next.next != -1 && !math.IsNaN(next.area) {
area := computeArea(items[next.prev].Point(pi), next.Point(pi), items[next.next].Point(pi))
next.area = area
heap.Fix(int(next.heapIdx))
}
}

q.d = append(q.d, MoveToCmd, items[first].X, items[first].Y, MoveToCmd)
point := items[first].Point(pi)
q.d = append(q.d, MoveToCmd, point.X, point.Y, MoveToCmd)
for i := items[first].next; i != -1 && i != first; i = items[i].next {
q.d = append(q.d, LineToCmd, items[i].X, items[i].Y, LineToCmd)
point = items[i].Point(pi)
q.d = append(q.d, LineToCmd, point.X, point.Y, LineToCmd)
}
if closed {
q.d = append(q.d, CloseCmd, items[first].X, items[first].Y, CloseCmd)
point = items[first].Point(pi)
q.d = append(q.d, CloseCmd, point.X, point.Y, CloseCmd)
}
}
return q
}

type heapVW []*itemVW

func (q *heapVW) Reset(capacity int) {
if capacity < cap(*q) {
*q = heapVW(make([]*itemVW, 0, capacity))
} else {
*q = (*q)[:0]
}
}

func (q heapVW) Init() {
n := len(q)
for i := n/2 - 1; 0 <= i; i-- {
q.down(i, n)
}
}

func (q *heapVW) Append(item *itemVW) {
item.heapIdx = int32(len(*q))
*q = append(*q, item)
}

func (q *heapVW) Push(item *itemVW) {
q.Append(item)
q.up(len(*q) - 1)
}

func (q *heapVW) Pop() *itemVW {
n := len(*q) - 1
q.swap(0, n)
q.down(0, n)

item := (*q)[n]
(*q) = (*q)[:n]
return item
}

func (q heapVW) Fix(i int) {
if !q.down(i, len(q)) {
q.up(i)
}
}

func (q heapVW) less(i, j int) bool {
return q[i].area < q[j].area
}

func (q heapVW) swap(i, j int) {
q[i], q[j] = q[j], q[i]
q[i].heapIdx, q[j].heapIdx = int32(i), int32(j)
}

// from container/heap
func (q heapVW) up(j int) {
for {
i := (j - 1) / 2 // parent
if i == j || !q.less(j, i) {
break
}
q.swap(i, j)
j = i
}
}

func (q heapVW) down(i0, n int) bool {
i := i0
for {
j1 := 2*i + 1
if n <= j1 || j1 < 0 { // j1 < 0 after int overflow
break
}
j := j1 // left child
if j2 := j1 + 1; j2 < n && q.less(j2, j1) {
j = j2 // = 2*i + 2 // right child
}
if !q.less(j, i) {
break
}
q.swap(i, j)
i = j
}
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 {
if x1 < x0 {
Expand Down

0 comments on commit 61aad66

Please sign in to comment.