-
Notifications
You must be signed in to change notification settings - Fork 2
/
QuickHull.ts
273 lines (221 loc) · 7.15 KB
/
QuickHull.ts
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
import { Vector } from "../types";
import { dim, hyperplaneFromPoints, signedDistToPlane } from "../math/VecMath";
import { Facet, Ridge } from "../geom/Geometry";
import { createSimplex } from "../geom/Simplex";
import { createCentroid, extendRidge } from "../geom/utils";
import {
removeElementOutOfOrder,
removeIndicesOutOfOrder,
shuffle
} from "@derschmale/array-utils";
import { EPSILON } from "../constants";
/**
* Some meta-data while constructing the facets
*/
class FacetInfo
{
outsideSet: number[] = [];
outsideDist: number[] = [];
currentPoint: Vector; // used to keep track of visibility tests
}
/**
* Gets the point in an outside set that's the furthest from the facet plane.
*
* @ignore
*/
function getFurthestPoint(facet: Facet): number
{
const { outsideSet, outsideDist } = facet.meta;
const len = outsideSet.length;
if (len === 0) return -1;
let p = outsideSet[0];
let dist = outsideDist[0];
for (let i = 1; i < len; ++i) {
if (outsideDist[i] > dist) {
dist = outsideDist[i];
p = outsideSet[i];
}
}
return p;
}
/**
* Assigns all points to the outside sets of a face.
*
* @ignore
*/
function generateOutsideSets(indices: number[], points: Vector[], facets: Facet[], dim: number)
{
let outsideSets = facets.map(_ => []);
const len = indices.length;
for (let i = 0; i < len; ++i) {
let index = indices[i];
const p = points[index];
for (let f of facets) {
let dist = signedDistToPlane(p, f.plane, dim);
if (dist > EPSILON) {
const meta = f.meta;
meta.outsideSet.push(index);
meta.outsideDist.push(dist);
break;
}
}
}
// any point now not in an outside set is inside the hull
return outsideSets;
}
/**
* Finds all faces visible to a point and their boundary ridges.
*
* @ignore
*/
function getVisibleSet(p: Vector, facet: Facet, visible: Facet[], horizon: Ridge[], dim: number)
{
visible.push(facet);
facet.meta.currentPoint = p;
for (let r of facet.ridges) {
const neighbor = r.neighbor.facet;
// already checked
if (neighbor.meta.currentPoint === p) continue;
if (signedDistToPlane(p, neighbor.plane, dim) > EPSILON)
getVisibleSet(p, neighbor, visible, horizon, dim);
else
horizon.push(r);
}
}
/**
* Builds a set of new facets for a point and its horizon ridges.
*
* @ignore
*/
function connectHorizonRidges(points: Vector[], index: number, H: Ridge[], centroid: Vector, dim: number): Facet[]
{
const newFacets = [];
// link horizon ridges with new point
for (let ridge of H) {
const newFacet = extendRidge(ridge, index, points, newFacets, centroid, dim);
newFacet.meta = new FacetInfo();
newFacets.push(newFacet);
}
return newFacets;
}
/**
* Returns the index with the "largest" point. The largest point is the one with the highest x coefficient, or y, z,
* etc. if equal
*
* @ignore
*/
function maximize(i: number, maxIndex: number, points: Vector[], d: number): number
{
const p = points[i];
const max = points[maxIndex];
for (let j = 0; j < d; ++j) {
if (p[j] < max[j]) return maxIndex;
if (p[j] > max[j]) return i;
}
// all the same: this only happens with duplicates, which shouldn't be in the set
return maxIndex;
}
/**
* @ignore
*/
function minimize(i: number, minIndex: number, points: Vector[], d: number): number
{
const p = points[i];
const min = points[minIndex];
for (let j = 0; j < d; ++j) {
if (p[j] > min[j]) return minIndex;
if (p[j] < min[j]) return i;
}
// all the same: this only happens with duplicates, which shouldn't be in the set
return minIndex;
}
/**
* Tries to find the biggest shape to start with
* @ignore
*/
function getOptimalStart(points: Vector[], d: number): number[]
{
const numPoints = points.length;
let minIndex = 0;
let maxIndex = 0;
// the initial axis
for (let i = 1; i < numPoints; ++i) {
maxIndex = maximize(i, maxIndex, points, d);
minIndex = minimize(i, minIndex, points, d);
}
const indices = [ minIndex, maxIndex ];
const planePts = [ points[minIndex], points[maxIndex] ];
// already have 2 points, need d + 1 in total
// in increasing dimensions, find the furthest from the current hyperplane
for (let i = 2; i < d + 1; ++i) {
const plane = hyperplaneFromPoints(planePts);
let maxDist = -Infinity;
let p = -1;
for (let j = 0; j < numPoints; ++j) {
const dist = Math.abs(signedDistToPlane(points[j], plane, i));
if (dist > maxDist) {
maxDist = dist;
p = j;
}
}
indices.push(p);
planePts.push(points[p]);
}
return indices;
}
/**
* QuickHull implements the algorithm of the same name, based on the original paper by Barber, Dobkin and Huhdanpaa.
* We're not interested in 0- or 1-dimensional cases (the latter can simply be the extent of the point values).
* QuickHull returns a set of indices into the original point list so we can map it to a different original ata set
* (fe: points may be a mapping for position vectors on some scene graph object).
*
* @author derschmale <http://www.derschmale.com>
*/
export function quickHull(points: Vector[]): Facet[]
{
const numPoints = points.length;
if (numPoints === 0) return;
const d = dim(points[0]);
if (numPoints <= d) {
throw new Error(`A convex hull in ${d} dimensions requires at least ${d + 1} points.`);
}
// initial unprocessed point indices:
const indices = [];
for (let i = 0; i < numPoints; ++i)
indices.push(i);
const simplexIndices = getOptimalStart(points, d);
const centroid = createCentroid(points, simplexIndices);
const facets = createSimplex(points, simplexIndices);
for (let f of facets)
f.meta = new FacetInfo();
removeIndicesOutOfOrder(indices, simplexIndices);
shuffle(indices);
generateOutsideSets(indices, points, facets, d);
// do not cache facets.length, as it will keep changing
let done = false;
// TODO: this extra loop should not be required
while (!done) {
done = true;
for (let i = 0; i < facets.length; ++i) {
const facet = facets[i];
const p = getFurthestPoint(facet);
if (p !== -1) {
removeElementOutOfOrder(facet.meta.outsideSet, p);
const V = [];
const H = [];
getVisibleSet(points[p], facet, V, H, d);
const newFacets = connectHorizonRidges(points, p, H, centroid, d);
for (let v of V) {
if (removeElementOutOfOrder(facets, v) <= i) --i;
generateOutsideSets(v.meta.outsideSet, points, newFacets, d);
if (v.meta.outsideSet.length > 0)
done = false;
}
facets.push(...newFacets);
}
}
}
for (let f of facets)
f.meta = null;
return facets;
}