-
Notifications
You must be signed in to change notification settings - Fork 0
/
polygon.go
293 lines (253 loc) · 8.15 KB
/
polygon.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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
package turdgl
import (
"container/ring"
"fmt"
"math"
"slices"
"time"
"github.com/netgusto/poly2tri-go"
)
// Polygon is a 2D shape with 3 or more sides.
type Polygon struct {
vertices []Vec
style Style
segments []*Triangle
}
// NewPolygon constructs a polygon from the specified vertices.
// The order of the vertices dictates the edges of the polygon.
// The final vertex is always linked to the first.
func NewPolygon(vecs []Vec) *Polygon {
return &Polygon{
vertices: vecs,
style: DefaultStyle,
segments: triangulatePoly2Tri(vecs),
}
}
// Move modifies the position of the polygon by the given vector.
func (p *Polygon) Move(mov Vec) {
// Translate every vertex by the move vector
for i := range p.vertices {
p.vertices[i] = Add(p.vertices[i], mov)
}
// Refresh the triangle segments
p.segments = triangulatePoly2Tri(p.vertices)
}
// Style returns a copy of the polygon's style.
func (p *Polygon) Style() Style {
return p.style
}
// SetStyle sets the style of a polygon.
func (p *Polygon) SetStyle(s Style) *Polygon {
p.style = s
return p
}
// Draw draws the polygon onto the provided frame buffer.
func (p *Polygon) Draw(buf *FrameBuffer) {
for _, segment := range p.segments {
if segment == nil {
fmt.Println("Segment nil error", time.Now())
} else {
segment.SetStyle(p.style)
segment.Draw(buf)
}
}
}
// vertex contains information about a vertex of a polygon.
type vertex struct {
pos Vec
isEar bool
}
// getVertex returns the value of the vertex in the given ring buffer node.
func getVertex(node *ring.Ring) vertex {
val, ok := node.Value.(vertex)
if !ok {
panic("failed to take ring.Value as type vertex")
}
return val
}
// triangulateEarClipping triangulates a polygon defined by a slice of vectors
// into a slice of drawable triangles using the ear clipping method.
func triangulateEarClipping(vecs []Vec) []*Triangle {
// Construct ring list of vertices for easier manipulation
r := ring.New(len(vecs))
for i := range r.Len() {
r.Value = vertex{pos: vecs[i]}
r = r.Next()
}
// Mark which vertices are ears
for range r.Len() {
calculateIsEar(r)
r = r.Next()
}
// Remove remove ears one by one, saving the triangle segment each time.
// Stop when there is only one triangle remaining. There are always 2 less
// triangles than polygon vertices
var segments []*Triangle
safetyCounter := 0
for r.Len() >= 3 {
safetyCounter++
if safetyCounter > 100 {
fmt.Println("Ear clipping emergency exit triggered due to not enough ears found")
break
}
// FIXME: this branch is sometimes not triggered enough times, meaning the loop would
// run forever without the emergency exit. This is because the algorithm fails to
// triangulate complex geometry properly. Run examples/snake/main.go to trigger the bug.
if getVertex(r).isEar {
// Save triangle
segments = append(segments, NewTriangle(
getVertex(r).pos,
getVertex(r.Prev()).pos,
getVertex(r.Next()).pos,
).SetStyle(RandomStyle()))
// Remove the ear vertex
r = r.Unlink(r.Len() - 1)
// Update states of adjacent vertices now the ear vertex has been removed.
calculateIsEar(r.Prev())
calculateIsEar(r.Next())
}
r = r.Next()
}
return segments
}
// isError keeps track of whether there's a polygon error.
var isError bool
// triangulatePoly2Tri triangulates a polygon defined by a slice of vectors
// into a slice of drawable triangles using Delauney triangulation.
// https://github.com/ByteArena/poly2tri-go
func triangulatePoly2Tri(vecs []Vec) []*Triangle {
// Convert vertices to poly2tri format
contour := make([]*poly2tri.Point, len(vecs))
for i, v := range vecs {
contour[i] = poly2tri.NewPoint(v.X, v.Y)
}
// Note: polygon holes can be added if needed using swctx.AddHole()
swctx := poly2tri.NewSweepContext(contour, false)
// Keep going if Triangulate() fails due to intersecting edges
defer func() {
msg := recover()
// This logic exists to not spam the logs. The error will only be
// printed once each time the polygon enters an error state
if msg == nil {
isError = false
} else if !isError {
isError = true
fmt.Println("Warning: Polygon error:", msg)
}
}()
swctx.Triangulate()
trianglesRaw := swctx.GetTriangles()
// Convert library format to turdgl triangles
var triangles []*Triangle
for _, t := range trianglesRaw {
a := Vec{t.Points[0].X, t.Points[0].Y}
b := Vec{t.Points[1].X, t.Points[1].Y}
c := Vec{t.Points[2].X, t.Points[2].Y}
triangles = append(triangles, NewTriangle(a, b, c))
}
return triangles
}
// calculateIsEar populates the isEar field of a single ring list element.
func calculateIsEar(vert *ring.Ring) {
current := getVertex(vert)
prev := getVertex(vert.Prev())
next := getVertex(vert.Next())
if !isConvex(current.pos, next.pos, prev.pos) {
return // ear vertices must be convex
}
// Check if any other vertices lie within the triangle formed by the
// vertex and its two neighbours.
triangle := NewTriangle(
getVertex(vert).pos,
getVertex(vert.Next()).pos,
getVertex(vert.Prev()).pos,
)
v := vert.Next()
for range v.Len() - 1 {
current := getVertex(v)
switch {
case slices.Contains([]Vec{triangle.v1, triangle.v2, triangle.v3}, current.pos):
// Ignore the triangle's own vertices
case triangle.pointInTriangle(current.pos):
// The vertex is not an ear if any other vertices exist in the triangle
v := getVertex(vert)
v.isEar = true
vert.Value = v
default:
// The vertex is an ear; set the isEar flag
v := getVertex(vert)
v.isEar = true
vert.Value = v
return
}
v = v.Next()
}
}
// isConvex returns true if the angle made by vectors ab and bc is convex,
// assuming that vectors ab and bc are oriented such that when you rotate the
// ab towards bc, the rotation is counterclockwise.
func isConvex(a, b, c Vec) bool {
return Cross(Sub(a, b), Sub(c, a)) > 0
}
// Triangle is triangle shape, defined by the position of its vertices.
type Triangle struct {
v1, v2, v3 Vec
style Style
}
// NewTriangle constructs a new triangle from the provided vertices.
func NewTriangle(v1, v2, v3 Vec) *Triangle {
return &Triangle{
v1: v1, v2: v2, v3: v3,
style: DefaultStyle,
}
}
// Style returns a copy of the triangle's style.
func (t *Triangle) Style() Style {
return t.style
}
// SetStyle sets the style of a triangle.
func (t *Triangle) SetStyle(s Style) *Triangle {
t.style = s
return t
}
// Draw rasterises and draws the triangle onto the provided frame buffer.
func (t *Triangle) Draw(buf *FrameBuffer) {
// Construct bounding box
maxX := math.Max(math.Max(t.v1.X, t.v2.X), t.v3.X)
minX := math.Min(math.Min(t.v1.X, t.v2.X), t.v3.X)
maxY := math.Max(math.Max(t.v1.Y, t.v2.Y), t.v3.Y)
minY := math.Min(math.Min(t.v1.Y, t.v2.Y), t.v3.Y)
bboxPos := Vec{minX, minY}
bbox := NewRect(maxX-minX, maxY-minY, bboxPos)
isClockwise := edgeFunction(t.v1, t.v2, t.v3) > 0
// Iterate over pixels in bounding box
for x := bbox.Pos.X; x <= bbox.Pos.X+bbox.w; x++ {
for y := bbox.Pos.Y; y <= bbox.Pos.Y+bbox.h; y++ {
p := Vec{float64(x), float64(y)}
ABP := edgeFunction(t.v1, t.v2, p)
BCP := edgeFunction(t.v2, t.v3, p)
CAP := edgeFunction(t.v3, t.v1, p)
// If the triangle is clockwise, check for all edge functions being >= 0.
// If it's anticlockwise, check for all edge functions being <= 0.
if (isClockwise && ABP >= 0 && BCP >= 0 && CAP >= 0) ||
(!isClockwise && ABP <= 0 && BCP <= 0 && CAP <= 0) {
jInt, iInt := int(math.Round(y)), int(math.Round(x))
buf.SetPixel(iInt, jInt, NewPixel(t.style.Colour))
}
}
}
}
// pointInTriangle returns true if point p exists within the area of the triangle.
// This function uses the barycentric coordinate method.
func (t *Triangle) pointInTriangle(p Vec) bool {
denominator := ((t.v2.Y-t.v3.Y)*(t.v1.X-t.v3.X) + (t.v3.X-t.v2.X)*(t.v1.Y-t.v3.Y))
a := ((t.v2.Y-t.v3.Y)*(p.X-t.v3.X) + (t.v3.X-t.v2.X)*(p.Y-t.v3.Y)) / denominator
b := ((t.v3.Y-t.v1.Y)*(p.X-t.v3.X) + (t.v1.X-t.v3.X)*(p.Y-t.v3.Y)) / denominator
c := 1 - a - b
return 0 <= a && a <= 1 && 0 <= b && b <= 1 && 0 <= c && c <= 1
}
// edgeFunction returns double the signed area of a triangle which has
// vertices a, b and c.
func edgeFunction(a, b, c Vec) float64 {
return (b.X-a.X)*(c.Y-a.Y) - (b.Y-a.Y)*(c.X-a.X)
}