Skip to content

Commit 394b634

Browse files
committed
Fix LaxPolygon to properly order vertices between shells and holes.
Clean up some comments and variable names. Update some of the methods to match current C++ logic. Add more unit tests for multiloop polygons to validate that holes are inverted ordering.
1 parent db83ebf commit 394b634

File tree

2 files changed

+198
-89
lines changed

2 files changed

+198
-89
lines changed

s2/lax_polygon.go

Lines changed: 75 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414

1515
package s2
1616

17+
import "slices"
18+
1719
// Shape interface enforcement
1820
var _ Shape = (*LaxPolygon)(nil)
1921

2022
// LaxPolygon represents a region defined by a collection of zero or more
2123
// closed loops. The interior is the region to the left of all loops. This
22-
// is similar to Polygon except that this class supports polygons
24+
// is similar to Polygon except that this type supports polygons
2325
// with degeneracies. Degeneracies are of two types: degenerate edges (from a
2426
// vertex to itself) and sibling edge pairs (consisting of two oppositely
2527
// oriented edges). Degeneracies can represent either "shells" or "holes"
@@ -28,50 +30,50 @@ var _ Shape = (*LaxPolygon)(nil)
2830
// degenerate hole. Such edges form part of the boundary of the polygon.
2931
//
3032
// Loops with fewer than three vertices are interpreted as follows:
31-
// - A loop with two vertices defines two edges (in opposite directions).
32-
// - A loop with one vertex defines a single degenerate edge.
33-
// - A loop with no vertices is interpreted as the "full loop" containing
34-
//
35-
// all points on the sphere. If this loop is present, then all other loops
36-
// must form degeneracies (i.e., degenerate edges or sibling pairs). For
37-
// example, two loops {} and {X} would be interpreted as the full polygon
38-
// with a degenerate single-point hole at X.
33+
// - A loop with two vertices defines two edges (in opposite directions).
34+
// - A loop with one vertex defines a single degenerate edge.
35+
// - A loop with no vertices is interpreted as the "full loop" containing
36+
// all points on the sphere. If this loop is present, then all other loops
37+
// must form degeneracies (i.e., degenerate edges or sibling pairs). For
38+
// example, two loops {} and {X} would be interpreted as the full polygon
39+
// with a degenerate single-point hole at X.
3940
//
4041
// LaxPolygon does not have any error checking, and it is perfectly fine to
4142
// create LaxPolygon objects that do not meet the requirements below (e.g., in
4243
// order to analyze or fix those problems). However, LaxPolygons must satisfy
4344
// some additional conditions in order to perform certain operations:
4445
//
45-
// - In order to be valid for point containment tests, the polygon must
46-
//
47-
// satisfy the "interior is on the left" rule. This means that there must
48-
// not be any crossing edges, and if there are duplicate edges then all but
49-
// at most one of them must belong to a sibling pair (i.e., the number of
50-
// edges in opposite directions must differ by at most one).
51-
//
52-
// - To be valid for polygon operations (BoundaryOperation), degenerate
46+
// - In order to be valid for point containment tests, the polygon must
47+
// satisfy the "interior is on the left" rule. This means that there must
48+
// not be any crossing edges, and if there are duplicate edges then all but
49+
// at most one of them must belong to a sibling pair (i.e., the number of
50+
// edges in opposite directions must differ by at most one).
5351
//
54-
// edges and sibling pairs cannot coincide with any other edges. For
55-
// example, the following situations are not allowed:
52+
// - To be valid for polygon operations (BooleanOperation), degenerate
53+
// edges and sibling pairs cannot coincide with any other edges. For
54+
// example, the following situations are not allowed:
5655
//
57-
// {AA, AA} // degenerate edge coincides with another edge
58-
// {AA, AB} // degenerate edge coincides with another edge
59-
// {AB, BA, AB} // sibling pair coincides with another edge
56+
// {AA, AA} // degenerate edge coincides with another edge
57+
// {AA, AB} // degenerate edge coincides with another edge
58+
// {AB, BA, AB} // sibling pair coincides with another edge
6059
//
6160
// Note that LaxPolygon is much faster to initialize and is more compact than
6261
// Polygon, but unlike Polygon it does not have any built-in operations.
63-
// Instead you should use ShapeIndex based operations such as BoundaryOperation,
62+
// Instead you should use ShapeIndex based operations such as BooleanOperation,
6463
// ClosestEdgeQuery, etc.
6564
type LaxPolygon struct {
6665
numLoops int
6766
vertices []Point
6867

69-
numVerts int
70-
cumulativeVertices []int
68+
numVerts int
69+
// when numLoops > 1, store a list of size (numLoops+1) where "i"
70+
// represents the total number of vertices in loops 0..i-1.
71+
loopStarts []int
72+
}
7173

72-
// TODO(roberts): C++ adds a prevLoop int field that claims to boost
73-
// chain position lookups by 1.5-4.5x. Benchmark to see if this
74-
// is useful here.
74+
// LaxPolygonFromLoops creates a LaxPolygon from the given Polygon.
75+
func LaxPolygonFromLoops(l []Loop) *LaxPolygon {
76+
return nil
7577
}
7678

7779
// LaxPolygonFromPolygon creates a LaxPolygon from the given Polygon.
@@ -85,7 +87,17 @@ func LaxPolygonFromPolygon(p *Polygon) *LaxPolygon {
8587
copy(spans[i], loop.vertices)
8688
}
8789
}
88-
return LaxPolygonFromPoints(spans)
90+
lax := LaxPolygonFromPoints(spans)
91+
92+
// Polygon and LaxPolygonShape holes are oriented oppositely, so we need
93+
// to reverse the orientation of any loops representing holes.
94+
for i := 0; i < p.NumLoops(); i++ {
95+
if p.Loop(i).IsHole() {
96+
v0 := lax.loopStarts[i]
97+
slices.Reverse(lax.vertices[v0 : v0+lax.numLoopVertices(i)])
98+
}
99+
}
100+
return lax
89101
}
90102

91103
// LaxPolygonFromPoints creates a LaxPolygon from the given points.
@@ -99,19 +111,18 @@ func LaxPolygonFromPoints(loops [][]Point) *LaxPolygon {
99111
case 1:
100112
p.numVerts = len(loops[0])
101113
p.vertices = make([]Point, p.numVerts)
114+
p.loopStarts = []int{0, 0}
102115
copy(p.vertices, loops[0])
103116
default:
104-
p.cumulativeVertices = make([]int, p.numLoops+1)
105-
numVertices := 0
117+
p.numVerts = 0
118+
p.loopStarts = make([]int, p.numLoops+1)
106119
for i, loop := range loops {
107-
p.cumulativeVertices[i] = numVertices
108-
numVertices += len(loop)
120+
p.loopStarts[i] = p.numVerts
121+
p.numVerts += len(loop)
122+
p.vertices = append(p.vertices, loop...)
109123
}
110124

111-
p.cumulativeVertices[p.numLoops] = numVertices
112-
for _, points := range loops {
113-
p.vertices = append(p.vertices, points...)
114-
}
125+
p.loopStarts[p.numLoops] = p.numVerts
115126
}
116127
return p
117128
}
@@ -121,58 +132,36 @@ func (p *LaxPolygon) numVertices() int {
121132
if p.numLoops <= 1 {
122133
return p.numVerts
123134
}
124-
return p.cumulativeVertices[p.numLoops]
135+
return p.loopStarts[p.numLoops]
125136
}
126137

127138
// numLoopVertices reports the total number of vertices in the given loop.
128139
func (p *LaxPolygon) numLoopVertices(i int) int {
129140
if p.numLoops == 1 {
130141
return p.numVerts
131142
}
132-
return p.cumulativeVertices[i+1] - p.cumulativeVertices[i]
143+
return p.loopStarts[i+1] - p.loopStarts[i]
133144
}
134145

135146
// loopVertex returns the vertex from loop i at index j.
136147
//
137148
// This requires:
138149
//
139150
// 0 <= i < len(loops)
140-
// 0 <= j < len(loop[i].vertices)
151+
// 0 <= j < numLoopVertices(i)
141152
func (p *LaxPolygon) loopVertex(i, j int) Point {
142153
if p.numLoops == 1 {
143154
return p.vertices[j]
144155
}
145156

146-
return p.vertices[p.cumulativeVertices[i]+j]
157+
return p.vertices[p.loopStarts[i]+j]
147158
}
148159

149160
func (p *LaxPolygon) NumEdges() int { return p.numVertices() }
150161

151162
func (p *LaxPolygon) Edge(e int) Edge {
152-
e1 := e + 1
153-
if p.numLoops == 1 {
154-
// wrap the end vertex if this is the last edge.
155-
if e1 == p.numVerts {
156-
e1 = 0
157-
}
158-
return Edge{p.vertices[e], p.vertices[e1]}
159-
}
160-
161-
// TODO(roberts): If this turns out to be performance critical in tests
162-
// incorporate the maxLinearSearchLoops like in C++.
163-
164-
// Check if e1 would cross a loop boundary in the set of all vertices.
165-
nextLoop := 0
166-
for p.cumulativeVertices[nextLoop] <= e {
167-
nextLoop++
168-
}
169-
170-
// If so, wrap around to the first vertex of the loop.
171-
if e1 == p.cumulativeVertices[nextLoop] {
172-
e1 = p.cumulativeVertices[nextLoop-1]
173-
}
174-
175-
return Edge{p.vertices[e], p.vertices[e1]}
163+
pos := p.ChainPosition(e)
164+
return p.ChainEdge(pos.ChainID, pos.Offset)
176165
}
177166

178167
func (p *LaxPolygon) Dimension() int { return 2 }
@@ -184,10 +173,11 @@ func (p *LaxPolygon) ReferencePoint() ReferencePoint { return referencePointForS
184173
func (p *LaxPolygon) NumChains() int { return p.numLoops }
185174
func (p *LaxPolygon) Chain(i int) Chain {
186175
if p.numLoops == 1 {
187-
return Chain{0, p.numVertices()}
176+
return Chain{Start: 0, Length: p.numVertices()}
188177
}
189-
start := p.cumulativeVertices[i]
190-
return Chain{start, p.cumulativeVertices[i+1] - start}
178+
179+
start := p.loopStarts[i]
180+
return Chain{Start: start, Length: p.loopStarts[i+1] - start}
191181
}
192182

193183
func (p *LaxPolygon) ChainEdge(i, j int) Edge {
@@ -196,29 +186,33 @@ func (p *LaxPolygon) ChainEdge(i, j int) Edge {
196186
if j+1 != n {
197187
k = j + 1
198188
}
189+
199190
if p.numLoops == 1 {
200-
return Edge{p.vertices[j], p.vertices[k]}
191+
return Edge{V0: p.vertices[j], V1: p.vertices[k]}
201192
}
202-
base := p.cumulativeVertices[i]
203-
return Edge{p.vertices[base+j], p.vertices[base+k]}
193+
194+
start := p.loopStarts[i]
195+
return Edge{V0: p.vertices[start+j], V1: p.vertices[start+k]}
204196
}
205197

206-
func (p *LaxPolygon) ChainPosition(e int) ChainPosition {
198+
func (p *LaxPolygon) ChainPosition(edgeID int) ChainPosition {
199+
207200
if p.numLoops == 1 {
208-
return ChainPosition{0, e}
201+
return ChainPosition{ChainID: 0, Offset: edgeID}
209202
}
210203

211-
// TODO(roberts): If this turns out to be performance critical in tests
212-
// incorporate the maxLinearSearchLoops like in C++.
213-
214-
// Find the index of the first vertex of the loop following this one.
215-
nextLoop := 1
216-
for p.cumulativeVertices[nextLoop] <= e {
204+
// We need the loopStart that is less than or equal to the edgeID.
205+
nextLoop := 0
206+
for p.loopStarts[nextLoop] <= edgeID {
217207
nextLoop++
218208
}
219209

220-
return ChainPosition{p.cumulativeVertices[nextLoop] - p.cumulativeVertices[1], e - p.cumulativeVertices[nextLoop-1]}
210+
return ChainPosition{
211+
ChainID: nextLoop - 1,
212+
Offset: edgeID - p.loopStarts[nextLoop-1],
213+
}
221214
}
222215

223216
// TODO(roberts): Remaining to port from C++:
224-
// EncodedLaxPolygon
217+
// encode/decode
218+
// Support for EncodedLaxPolygon

0 commit comments

Comments
 (0)