Skip to content

Commit 05b10c8

Browse files
authored
common: fix regression with Inferno convex hulls (#87)
- Return ordered r2.Point slice for 2D hull - Return quickhull.ConvexHull for 3D hull
1 parent 247992e commit 05b10c8

File tree

5 files changed

+91
-28
lines changed

5 files changed

+91
-28
lines changed

common/inferno.go

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ package common
22

33
import (
44
"math/rand"
5+
"sort"
56

6-
quickhull "github.com/markus-wa/quickhull-go"
7-
7+
r2 "github.com/golang/geo/r2"
88
r3 "github.com/golang/geo/r3"
9+
quickhull "github.com/markus-wa/quickhull-go"
910
)
1011

1112
// Inferno is a list of Fires with helper functions.
@@ -51,31 +52,89 @@ func (inf Inferno) Active() Inferno {
5152
return res
5253
}
5354

54-
// ConvexHull2D returns the vertices making up the 2D convex hull of all the fires in the inferno.
55+
// ConvexHull2D returns clockwise sorted corner points making up the 2D convex hull of all the fires in the inferno.
5556
// Useful for drawing on 2D maps.
56-
func (inf Inferno) ConvexHull2D() []r3.Vector {
57-
pointCloud := make([]r3.Vector, 0, len(inf.Fires))
57+
func (inf Inferno) ConvexHull2D() []r2.Point {
58+
pointCloud := make([]r3.Vector, len(inf.Fires))
59+
for i, f := range inf.Fires {
60+
pointCloud[i] = f.Vector
61+
pointCloud[i].Z = 0
62+
}
5863

59-
for _, f := range inf.Fires {
60-
pointCloud = append(pointCloud, r3.Vector{
61-
X: f.Vector.X,
62-
Y: f.Vector.Y,
63-
Z: 0,
64-
})
64+
vertices := convexHull(convexHull(pointCloud).Vertices).Vertices
65+
points := make([]r2.Point, len(vertices))
66+
for i, v := range vertices {
67+
points[i] = r2.Point{X: v.X, Y: v.Y}
68+
}
69+
sortPointsClockwise(points)
70+
71+
return points
72+
}
73+
74+
// pointsClockwiseSorter implements the Sort interface for slices of Point
75+
// with a comparator for sorting points in clockwise order around their center.
76+
type pointsClockwiseSorter struct {
77+
center r2.Point
78+
points []r2.Point
79+
}
80+
81+
func (s pointsClockwiseSorter) Len() int { return len(s.points) }
82+
83+
func (s pointsClockwiseSorter) Swap(i, j int) { s.points[i], s.points[j] = s.points[j], s.points[i] }
84+
85+
func (s pointsClockwiseSorter) Less(i, j int) bool {
86+
a, b := s.points[i], s.points[j]
87+
88+
if a.X-s.center.X >= 0 && b.X-s.center.X < 0 {
89+
return true
90+
}
91+
if a.X-s.center.X < 0 && b.X-s.center.X >= 0 {
92+
return false
93+
}
94+
if a.X-s.center.X == 0 && b.X-s.center.X == 0 {
95+
if a.Y-s.center.Y >= 0 || b.Y-s.center.Y >= 0 {
96+
return a.Y > b.Y
97+
}
98+
return b.Y > a.Y
99+
}
100+
101+
// compute the cross product of vectors (s.center -> a) X (s.center -> b)
102+
det := (a.X-s.center.X)*(b.Y-s.center.Y) - (b.X-s.center.X)*(a.Y-s.center.Y)
103+
if det < 0 {
104+
return true
105+
}
106+
if det > 0 {
107+
return false
65108
}
66109

67-
return quickhull.ConvexHull(pointCloud)
110+
// points a and b are on the same line from the s.center
111+
// check which point is closer to the s.center
112+
d1 := (a.X-s.center.X)*(a.X-s.center.X) + (a.Y-s.center.Y)*(a.Y-s.center.Y)
113+
d2 := (b.X-s.center.X)*(b.X-s.center.X) + (b.Y-s.center.Y)*(b.Y-s.center.Y)
114+
return d1 > d2
68115
}
69116

70-
// ConvexHull3D returns the vertices making up the 3D convex hull of all the fires in the inferno.
71-
func (inf Inferno) ConvexHull3D() []r3.Vector {
117+
func sortPointsClockwise(points []r2.Point) {
118+
sorter := pointsClockwiseSorter{
119+
center: r2.RectFromPoints(points...).Center(),
120+
points: points,
121+
}
122+
sort.Sort(sorter)
123+
}
124+
125+
// ConvexHull3D returns the 3D convex hull of all the fires in the inferno.
126+
func (inf Inferno) ConvexHull3D() quickhull.ConvexHull {
72127
pointCloud := make([]r3.Vector, len(inf.Fires))
73128

74129
for i, f := range inf.Fires {
75130
pointCloud[i] = f.Vector
76131
}
77132

78-
return quickhull.ConvexHull(pointCloud)
133+
return convexHull(pointCloud)
134+
}
135+
136+
func convexHull(pointCloud []r3.Vector) quickhull.ConvexHull {
137+
return new(quickhull.QuickHull).ConvexHull(pointCloud, false, false, 0)
79138
}
80139

81140
// NewInferno creates a inferno and sets the Unique-ID.

common/inferno_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package common
33
import (
44
"testing"
55

6+
r2 "github.com/golang/geo/r2"
67
r3 "github.com/golang/geo/r3"
78
assert "github.com/stretchr/testify/assert"
89
)
@@ -65,16 +66,16 @@ func TestInfernoConvexHull2D(t *testing.T) {
6566
},
6667
}
6768

68-
expectedHull := []r3.Vector{
69-
{X: 1, Y: 2, Z: 0},
70-
{X: 4, Y: 7, Z: 0},
71-
{X: 7, Y: 2, Z: 0},
69+
expectedHull := []r2.Point{
70+
{X: 1, Y: 2},
71+
{X: 4, Y: 7},
72+
{X: 7, Y: 2},
7273
}
7374

7475
assert.ElementsMatch(t, expectedHull, inf.ConvexHull2D(), "ConvexHull2D should be as expected")
7576

7677
// 3D-hull should be different
77-
assert.NotEqual(t, len(expectedHull), len(inf.ConvexHull3D()), "3D hull should contain the vertex 'D'")
78+
assert.NotEqual(t, len(expectedHull), len(inf.ConvexHull3D().Vertices), "3D hull should contain the vertex 'D'")
7879
}
7980

8081
// Just check that all fires are passed to quickhull.ConvexHull()
@@ -103,5 +104,5 @@ func TestInfernoConvexHull3D(t *testing.T) {
103104
{X: 4, Y: 4, Z: 12},
104105
}
105106

106-
assert.ElementsMatch(t, expectedHull, inf.ConvexHull3D(), "ConvexHull3D should contain all fire locations")
107+
assert.ElementsMatch(t, expectedHull, inf.ConvexHull3D().Vertices, "ConvexHull3D should contain all fire locations")
107108
}

examples/nade-trajectories/nade_trajectories.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
_ "image/jpeg"
1010
"os"
1111

12+
"github.com/golang/geo/r2"
1213
"github.com/golang/geo/r3"
1314
"github.com/llgcode/draw2d/draw2dimg"
1415

@@ -135,7 +136,7 @@ func drawInfernos(gc *draw2dimg.GraphicContext, infernos []*common.Inferno) {
135136
gc.SetFillColor(colorInferno)
136137

137138
// Calculate hulls
138-
hulls := make([][]r3.Vector, len(infernos))
139+
hulls := make([][]r2.Point, len(infernos))
139140
for i := range infernos {
140141
hulls[i] = infernos[i].ConvexHull2D()
141142
}
@@ -155,16 +156,16 @@ func drawInfernos(gc *draw2dimg.GraphicContext, infernos []*common.Inferno) {
155156
}
156157
}
157158

158-
func buildInfernoPath(gc *draw2dimg.GraphicContext, vertices []r3.Vector) {
159-
x, y := curMap.TranslateScale(vertices[0].X, vertices[0].Y)
160-
gc.MoveTo(x, y)
159+
func buildInfernoPath(gc *draw2dimg.GraphicContext, vertices []r2.Point) {
160+
xOrigin, yOrigin := curMap.TranslateScale(vertices[0].X, vertices[0].Y)
161+
gc.MoveTo(xOrigin, yOrigin)
161162

162163
for _, fire := range vertices[1:] {
163-
x, y = curMap.TranslateScale(fire.X, fire.Y)
164+
x, y := curMap.TranslateScale(fire.X, fire.Y)
164165
gc.LineTo(x, y)
165166
}
166167

167-
gc.LineTo(x, y)
168+
gc.LineTo(xOrigin, yOrigin)
168169
}
169170

170171
func drawTrajectories(gc *draw2dimg.GraphicContext, trajectories []*nadePath) {

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ require (
99
github.com/llgcode/ps v0.0.0-20150911083025-f1443b32eedb // indirect
1010
github.com/markus-wa/gobitread v0.2.2
1111
github.com/markus-wa/godispatch v1.1.0
12-
github.com/markus-wa/quickhull-go v1.0.1
12+
github.com/markus-wa/quickhull-go v0.0.0-20190116183559-9fb9702adbda
1313
github.com/stretchr/objx v0.1.1 // indirect
1414
github.com/stretchr/testify v1.2.2
1515
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ github.com/markus-wa/gobitread v0.2.2 h1:4Z4oJ8Bf1XnOy6JZ2/9AdFKVAoxdq7awRjrb+j2
1616
github.com/markus-wa/gobitread v0.2.2/go.mod h1:PcWXMH4gx7o2CKslbkFkLyJB/aHW7JVRG3MRZe3PINg=
1717
github.com/markus-wa/godispatch v1.1.0 h1:J8O+hRkOCexDUQevaSKWDtKeZ3+HcmbEUKY1uYraAjY=
1818
github.com/markus-wa/godispatch v1.1.0/go.mod h1:6o18u24oo58yseMXYD0zQFI6LbSkjJSSBQ4YyDqFX5c=
19+
github.com/markus-wa/quickhull-go v0.0.0-20190116183559-9fb9702adbda h1:F7blFtp+y3qD7GE+9mY3YLtUGnmE+6CSlwwAxEl/q5Y=
20+
github.com/markus-wa/quickhull-go v0.0.0-20190116183559-9fb9702adbda/go.mod h1:gMPnFb0DpuzRpbHesp64Nq4oFXE5SglAD86nlKrkETs=
1921
github.com/markus-wa/quickhull-go v1.0.1 h1:msrwpA4mlVI2Rww6ZzQuCxv+axXVknMaMJYO1om37Mk=
2022
github.com/markus-wa/quickhull-go v1.0.1/go.mod h1:gMPnFb0DpuzRpbHesp64Nq4oFXE5SglAD86nlKrkETs=
2123
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

0 commit comments

Comments
 (0)