-
Notifications
You must be signed in to change notification settings - Fork 20
/
polyline.go
188 lines (154 loc) · 4.93 KB
/
polyline.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
package cp
import "math"
type PolyLine struct {
Verts []Vector
}
type PolyLineSet struct {
Lines []*PolyLine
}
func Next(i, count int) int {
return (i + 1) % count
}
func Sharpness(a, b, c Vector) float64 {
return a.Sub(b).Normalize().Dot(c.Sub(b).Normalize())
}
func (pl *PolyLine) Push(v Vector) *PolyLine {
pl.Verts = append(pl.Verts, v)
return pl
}
func (pl *PolyLine) Enqueue(v Vector) *PolyLine {
pl.Verts = append([]Vector{v}, pl.Verts...)
return pl
}
func (pl *PolyLine) IsClosed() bool {
return len(pl.Verts) > 1 && pl.Verts[0].Equal(pl.Verts[len(pl.Verts)-1])
}
func (pl *PolyLine) IsShort(count, start, end int, min float64) bool {
var length float64
// Next doesn't have side effects and its return value is ignored (SA4017)go-staticcheck
for i := start; i != end; Next(i, count) {
length += pl.Verts[i].Distance(pl.Verts[Next(i, count)])
if length > min {
return false
}
}
return true
}
// Join similar adjacent line segments together. Works well for hard edged shapes.
// 'tol' is the minimum anglular difference in radians of a vertex.
func (pl *PolyLine) SimplifyVertexes(tol float64) *PolyLine {
reduced := PolyLine{Verts: []Vector{pl.Verts[0], pl.Verts[1]}}
minSharp := -math.Cos(tol)
for i := 2; i < len(pl.Verts); i++ {
vert := pl.Verts[i]
sharp := Sharpness(reduced.Verts[len(reduced.Verts)-2], reduced.Verts[len(reduced.Verts)-1], vert)
if sharp <= minSharp {
reduced.Verts[len(reduced.Verts)-1] = vert
} else {
reduced.Push(vert)
}
}
if pl.IsClosed() && Sharpness(reduced.Verts[len(reduced.Verts)-2], reduced.Verts[0], reduced.Verts[1]) < minSharp {
reduced.Verts[0] = reduced.Verts[len(reduced.Verts)-2]
}
return &reduced
}
// Recursive function used by cpPolylineSimplifyCurves().
func DouglasPeucker(verts []Vector, reduced *PolyLine, length, start, end int, min, tol float64) *PolyLine {
// Early exit if the points are adjacent
if (end-start+length)%length < 2 {
return reduced
}
a := verts[start]
b := verts[end]
// Check if the length is below the threshold
if a.Near(b, min) && reduced.IsShort(length, start, end, min) {
return reduced
}
// Find the maximal vertex to split and recurse on
var max float64
maxi := start
n := b.Sub(a).Perp().Normalize()
d := n.Dot(a)
for i := Next(start, length); i != end; i = Next(i, length) {
dist := math.Abs(n.Dot(verts[i]) - d)
if dist > max {
max = dist
maxi = i
}
}
if max > tol {
reduced = DouglasPeucker(verts, reduced, length, start, maxi, min, tol)
reduced.Push(verts[maxi])
reduced = DouglasPeucker(verts, reduced, length, maxi, end, min, tol)
}
return reduced
}
// Recursively reduce the vertex count on a polyline. Works best for smooth shapes.
// 'tol' is the maximum error for the reduction.
// The reduced polyline will never be farther than this distance from the original polyline.
func (pl *PolyLine) SimplifyCurves(tol float64) *PolyLine {
reduced := &PolyLine{}
min := tol / 2.0
if pl.IsClosed() {
start, end := LoopIndexes(pl.Verts, len(pl.Verts)-1)
reduced = reduced.Push(pl.Verts[start])
reduced = DouglasPeucker(pl.Verts, reduced, len(pl.Verts)-1, start, end, min, tol)
reduced = reduced.Push(pl.Verts[end])
reduced = DouglasPeucker(pl.Verts, reduced, len(pl.Verts)-1, end, start, min, tol)
reduced = reduced.Push(pl.Verts[start])
} else {
reduced = reduced.Push(pl.Verts[0])
reduced = DouglasPeucker(pl.Verts, reduced, len(pl.Verts), 0, len(pl.Verts)-1, min, tol)
reduced = reduced.Push(pl.Verts[len(pl.Verts)-1])
}
return reduced
}
// Find the polyline that ends with v.
func (pls *PolyLineSet) FindEnds(v Vector) int {
for i, line := range pls.Lines {
if line.Verts != nil && line.Verts[len(line.Verts)-1].Equal(v) {
return i
}
}
return -1
}
// Find the polyline that starts with v.
func (pls *PolyLineSet) FindStarts(v Vector) int {
for i, line := range pls.Lines {
if line.Verts != nil && line.Verts[0].Equal(v) {
return i
}
}
return -1
}
// Add a new polyline to a polyline set.
func (pls *PolyLineSet) Push(v *PolyLine) {
pls.Lines = append(pls.Lines, v)
}
// Join two cpPolylines in a polyline set together.
// this may deletion could be slow? https://yourbasic.org/golang/delete-element-slice/
func (pls *PolyLineSet) Join(before, after int) {
pls.Lines[before].Verts = append(pls.Lines[before].Verts, pls.Lines[after].Verts...)
copy(pls.Lines[after:], pls.Lines[after+1:])
pls.Lines = pls.Lines[:len(pls.Lines)-1]
}
// Add a segment to a polyline set.
// A segment will either start a new polyline, join two others, or add to or loop an existing polyline.
func PolyLineCollectSegment(v0, v1 Vector, pls *PolyLineSet) {
before := pls.FindEnds(v0)
after := pls.FindStarts(v1)
if before >= 0 && after >= 0 {
if before == after {
pls.Lines[before].Push(v1)
} else {
pls.Join(before, after)
}
} else if before >= 0 {
pls.Lines[before].Push(v1)
} else if after >= 0 {
pls.Lines[after].Enqueue(v0)
} else {
pls.Push(&PolyLine{Verts: []Vector{v0, v1}})
}
}