-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathqhull.lua
159 lines (147 loc) · 5.51 KB
/
qhull.lua
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
local Vec = require(table.concat({DEWALL_PATH, 'vector-light'}, '.'))
-- Get a bunch of points that look like this: {x = n, y = o}
-- Localization
local push, pop = table.insert, table.remove
local atan2 = math.atan2
-- Remove an element without creating a "hole" and return the tail
local function swapop(table, index)
if not index then return nil end
table[index], table[#table] = table[#table], table[index]
return pop(table)
end
-- Generate point-index array
local function new_p_array(points)
local p_array = {}
for i = 1,#points do
p_array[i] = i -- order doesn't matter, value is pointer to points table
end
return p_array
end
-- Given an array of points, find the minimum
local function min_point(points, p_array)
local p_test, p_min, mini
for i, pointer in ipairs(p_array) do
p_test = points[pointer]
if not mini or p_test.x < p_min.x or (p_test.x == p_min.x and p_test.y < p_min.y) then
mini = i
p_min = points[pointer]
end
end
return swapop(p_array, mini)
end
-- Given an array of points, find the maximum
local function max_point(points, p_array)
local p_test, p_max, maxi
for i, pointer in ipairs(p_array) do
p_test = points[pointer]
if not maxi or p_test.x > p_max.x or (p_test.x == p_max.x and p_test.y > p_max.y) then
maxi = i
p_max = points[pointer]
end
end
return swapop(p_array, maxi)
end
-- Given an array of points, return the two with min/max x values
local function min_max_points(points, p_array)
return min_point(points, p_array), max_point(points, p_array)
end
-- Test if 3 points make a ccw turn
local function is_ccw(p, q, r)
return Vec.det(q.x-p.x, q.y-p.y, r.x-p.x, r.y-p.y) > 0
end
-- Planar distance function
local function point_plane_dist(points, pmin, pmax, p)
local plane_x, plane_y = Vec.sub(points[pmax].x, points[pmax].y, points[pmin].x, points[pmin].y)
local p_vec_x, p_vec_y = Vec.sub(points[p].x, points[p].y, points[pmin].x, points[pmin].y)
-- Reject p_vec onto plane vector and return length
return Vec.len( Vec.reject(p_vec_x, p_vec_y, plane_x, plane_y) )
end
-- Given points and min/max points or simplex
-- If not simplex: Partition points into two subsets on either side of line formed by min/max
-- If simplex: Delete points in simplex, partition remaining into 2 subsets
local function partition(points,p_array, p1,p2,pmax)
local p_test
local p_1, p_2 = {}, {}
local dist_1,max_dist_1, p_1_max
local dist_2,max_dist_2, p_2_max
local p_start, p_end = pmax and p2 or p1, pmax or p2
for i = #p_array,1,-1 do
p_test = p_array[i]
if is_ccw(points[p1], points[p_end], points[p_test]) then -- wins if collinear
push(p_1, swapop(p_array, i))
dist_1 = point_plane_dist(points, p1,p_end,p_test)
if not p_1_max or dist_1 > max_dist_1 then
max_dist_1 = dist_1
p_1_max = #p_1 -- i is the max and we just added it to p1
end
elseif not pmax or is_ccw(points[p_end], points[p_start], points[p_test]) then
push(p_2, swapop(p_array, i))
dist_2 = point_plane_dist(points,p_end,p_start,p_test)
if not p_2_max or dist_2 > max_dist_2 then
max_dist_2 = dist_2
p_2_max = #p_2
end
else
swapop(p_array, i)
end
end
return p_1, swapop(p_1,p_1_max), p_2, swapop(p_2,p_2_max)
end
local function find_hull(hull, points, p_array, p1, p2, p_max)
-- Add max point to hull
push(hull, p_max)
if #p_array == 0 then return end
-- Partition remaining points
local p_1, p_1_max, p_2, p_2_max = partition(points, p_array, p1,p2,p_max)
-- Recurse for two new lines: p1-pmax, pmax-p2
find_hull(hull, points, p_1, p1, p_max, p_1_max)
find_hull(hull, points, p_2, p_max, p2, p_2_max)
end
local function sort_hull(hull, points)
-- Find reference point to calculate cw/ccw from (left-most x, lowest y)
local p_ref = points[hull[1]]
for i = 2, #hull do
local p_test = points[hull[i]]
if p_test.y < p_ref.y or (p_test.y == p_ref.y and p_test.x < p_ref.x) then
p_ref = points[hull[i]]
end
end
-- table.sort function - sorts points in ccw order
-- p_ref and points_indices are upvalues (within scope); can be accessed from table.sort
local function sort_ccw_i(v1,v2)
v1,v2 = points[v1], points[v2]
if v1.x == p_ref.x and v1.y == p_ref.y then
return true -- if v1 is p_ref, then it should win the sort automatically
elseif v2.x == p_ref.x and v2.y == p_ref.y then
return false -- if v2 is p_ref, then v1 should lose the sort automatically
end
-- Else compute and compare polar angles
local a1 = atan2(v1.y - p_ref.y, v1.x - p_ref.x) -- angle between x axis and line from p_ref to v1
local a2 = atan2(v2.y - p_ref.y, v2.x - p_ref.x) -- angle between x axis and line from p_ref to v1
if a1 < a2 then
return true -- true means first arg wins the sort (v1 in our case)
elseif a1 == a2 then -- points have same angle, so choose the point furthest from p_ref
local m1 = Vec.dist(v1.x,v1.y, p_ref.x,p_ref.y)
local m2 = Vec.dist(v2.x,v2.y, p_ref.x,p_ref.y)
if m1 > m2 then
return true -- v1 is further away, so it wins the sort
end
end
end
-- Sort points
table.sort(hull, sort_ccw_i)
return hull
end
local function qhull(points)
local hull = {}
local p_array = new_p_array(points)
local pmin, pmax = min_max_points(points, p_array)
-- Add to hull
push(hull, pmin); push(hull, pmax)
-- Partition points and recursively generate hull for both subdomains
local p_1, p_1_max, p_2, p_2_max = partition(points, p_array, pmin, pmax)
find_hull(hull, points, p_1, pmin, pmax, p_1_max)
find_hull(hull, points, p_2, pmax, pmin, p_2_max)
return sort_hull(hull, points)
end
return qhull