-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpocket.js
491 lines (491 loc) · 16.9 KB
/
pocket.js
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
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
/*!
Pocket v2.0.0
(c) 2020 Trevor Richard
Released under the MIT License.
*/
/**
* A particle object that consists of both positional data and a custom data payload.
*/
export class Particle {
constructor({ x, y, z = 0, radius = 0, data }) {
this._radius = 0;
this._x = x;
this._y = y;
this._z = z;
this.radius = radius;
if (data !== undefined)
this.data = data;
}
/**
* Safely moves the particle's position in the Pocket.
* @param position The vector position to move the Particle to.
*/
moveTo(position) {
if (!position.z)
position.z = 0;
if (this._pocket && this._subPocket) {
this._subPocket.retrieve(this);
this._x = position.x;
this._y = position.y;
this._z = position.z;
this._pocket.put(this);
}
else {
this._x = position.x;
this._y = position.y;
this._z = position.z;
}
}
/**
* Retrieves the particle from its pocket if it exists in one.
* @return `this`
*/
retrieve() {
var _a;
(_a = this._subPocket) === null || _a === void 0 ? void 0 : _a.retrieve(this);
this._subPocket = undefined;
this._pocket = undefined;
return this;
}
/**
* `INTERNAL USE ONLY.` Sets the pocket that this particle belongs to.
* @dev Do not change the pocket manually. Instead, use the `Pocket.put(...)` function.
*/
__setPocket(pocket) {
if (this._pocket && this._pocket != pocket)
throw Object.assign({ particle: this }, new Error("Particle is already in a different pocket. It must be retrieved before putting it in a new pocket"));
this._pocket = pocket;
}
/**
* `INTERNAL USE ONLY.` Sets the sub pocket that this particle belongs to.
* @dev Do not change the sub pocket manually. Instead, use the moveTo(...) function
* or equivalent positional setters.
*/
__setSubPocket(subPocket) {
this._subPocket = subPocket;
}
/**
* Getter for the 'x' coordinate of the particle.
* @return The 'x' position of the particle.
*/
get x() {
return this._x;
}
/**
* Getter for the 'y' coordinate of the particle.
* @return The 'y' position of the particle.
*/
get y() {
return this._y;
}
/**
* Getter for the 'z' coordinate of the particle.
* @return The 'z' position of the particle.
*/
get z() {
return this._z;
}
/**
* Getter for the radius of the particle.
* @return The radius of the particle.
*/
get radius() {
return this._radius;
}
/**
* Getter for the pocket of the particle.
* @return The pocket that the particle belongs to, or `undefined` if it is not in a pocket.
*/
get pocket() {
return this._pocket;
}
/**
* Safely sets the 'x' coordinate of the particle.
* @param value The new 'x' position.
*/
set x(value) {
this.moveTo({
x: value,
y: this.y,
z: this.z
});
}
/**
* Safely sets the 'y' coordinate of the particle.
* @param value The new 'y' position.
*/
set y(value) {
this.moveTo({
x: this.x,
y: value,
z: this.z
});
}
/**
* Safely sets the 'z' coordinate of the particle.
* @param value The new 'z' position.
*/
set z(value) {
this.moveTo({
x: this.x,
y: this.y,
z: value
});
}
/**
* Setter for the radius of the particle.
* @param value The new radius of the particle.
* @dev If a negative radius is given, it's positive counterpart will be used instead.
*/
set radius(value) {
if (value < 0)
value *= -1; // ensure radius is positive
if (this._pocket && this._subPocket) {
this._subPocket.retrieve(this);
this._radius = value;
this._pocket.put(this);
}
else {
this._radius = value;
}
}
}
/**
* SubPockets are used to organize particles into sub-spaces that enable efficient positional queries.
* @dev SubPockets are meant for internal use only and are not optimized for external interfacing.
*/
class SubPocket {
constructor({ parent, radius, position }) {
this.parent = parent;
this.radius = radius;
this.pocket = parent instanceof Pocket ? parent : parent.pocket;
this.subPockets = new Array();
this.particles = new Array();
this.position = position;
}
/**
* Places the particle in this pocket or in a sub pocket of this pocket and returns the SubPocket it was placed in.
* @param p The Particle to put in the SubPocket
* @return The particle that was placed, or undefined if the particle
*/
put(p) {
const diff = Pocket.Tools.sub(this.position, p);
const dist = Pocket.Tools.mag(diff);
if (dist + p.radius < this.radius) {
if (p.radius >= this.radius / Pocket.Tools.MAGIC_RATIO || this.particles.length == 0) {
// Add object to the pocket
this.particles.push(p);
// Set particle SubPocket
p.__setSubPocket(this);
// Return this SubPocket
return this;
}
else {
// Add object to a SubPocket
for (let i = 0; i < this.subPockets.length; i++) {
const successfullPocket = this.subPockets[i].put(p);
if (successfullPocket)
return successfullPocket;
}
// Doesn't fit in any SubPockets so we will create a new one with half the radius of this one, centered at the object's position
const sp = new SubPocket({
parent: this,
radius: this.radius / Pocket.Tools.MAGIC_RATIO,
position: {
x: p.x,
y: p.y,
z: p.z
}
});
this.subPockets.push(sp);
return sp.put(p);
}
}
else {
return undefined;
}
}
/**
* Retrieves a particle from this pocket if it is stored here and removes it.
* @param p The particle to retrieve from this pocket
* @dev If the subpocket is empty after retrieval (successful or unsuccessful), it will be removed from its parent pocket.
* @return True if the particle was retrieved, false otherwise.
*/
retrieve(p) {
let numParticlesBefore = this.particles.length;
this.particles = this.particles.filter(pp => pp != p);
let retrieved = numParticlesBefore > this.particles.length;
if (retrieved) {
this.pocket.__particleRemoved();
}
if (this.subPockets.length == 0 && this.particles.length == 0) {
this.parent.remove(this);
}
return retrieved;
}
/**
* Removes a SubPocket from this pocket.
* @param sp The SubPocket to be removed
* @dev If this sub pocket is empty after removal (successful or unsuccessful), it will be removed from its parent pocket.
* @return True if the sub pocket was found & removed, false otherwise.
*/
remove(sp) {
let numPocketsBefore = this.subPockets.length;
this.subPockets = this.subPockets.filter(p => p != sp);
let removed = numPocketsBefore > this.subPockets.length;
if (this.subPockets.length == 0 && this.particles.length == 0) {
this.parent.remove(this);
}
return removed;
}
/**
* Returns an array of all Particles that exist wholly or partially within the given radius of the desired coordinates.
* @param radius The radius of the search
* @param center The 2D or 3D vector coordinates of the center of the search
* @param set The shared set of particles to add to
* @param transform The optional transform function to apply to the particles before checking their inclusion
*/
search(radius, center, set, transform) {
const pos = transform ? transform(this.position) : this.position;
const diff = Pocket.Tools.sub(pos, center);
const dist = Pocket.Tools.mag(diff);
if (dist - radius < this.radius) {
// Search this pocket's particles
this.particles.forEach(p => {
const p_pos = transform ? transform(p) : p;
const p_diff = Pocket.Tools.sub(p_pos, center);
const p_dist = Pocket.Tools.mag(p_diff);
if (p_dist - radius < p.radius) {
set.add(p);
}
});
// Search this pocket's SubPockets
this.subPockets.forEach(pocket => {
pocket.search(radius, center, set, transform);
});
}
return set;
}
}
/**
* Pockets are organizational structures for Particle objects that enable efficient positional queries by
* dynamically grouping particles into recursively smaller sub pockets.
* @dev The searching, moving, and removal of particles are all approximately O(ln(n)) operations, providing
* extremely efficient positional lookups of particles at any positional scale.
*/
export class Pocket {
constructor() {
this._root = undefined;
this._count = 0;
}
/**
* Puts a particle into this pocket.
* @param particle The particle to put into this pocket.
* @dev The particle MUST NOT already be in a different pocket. If it is,
* it must be retrieved before calling this function.
* @return The particle that was placed into this pocket.
*/
put(particle) {
// Set the particle's pocket reference
particle.__setPocket(this);
// Try to place the Particle in the current root
if (this._root) {
if (this._root.put(particle))
return particle;
}
// Either root does not exist, or put failed, so create a custom pocket for the particle
const sp_radius = Pocket.Tools.MAGIC_RATIO * (particle.radius || 1);
const sp = new SubPocket({
parent: this,
radius: this._root ? Math.max(this._root.radius, sp_radius) : sp_radius,
position: {
x: particle.x,
y: particle.y,
z: particle.z
}
});
if (!this._root) {
this._root = sp;
}
else {
// Create a new root that encompasses both the old root and new SubPocket
const max_dist = Pocket.Tools.mag(Pocket.Tools.sub(this._root.position, sp.position)) + sp.radius; // The distance from the current root to the outside of the new SubPocket
const new_root = new SubPocket({
parent: this,
radius: Pocket.Tools.MAGIC_RATIO * max_dist,
position: this._root.position
});
// Set the parents of the old root and new SubPocket to the new root
this._root.parent = new_root;
sp.parent = new_root;
// Add the old root and new SubPocket to the new root
new_root.subPockets.push(this._root);
new_root.subPockets.push(sp);
// Set the new root
this._root = new_root;
}
if (!sp.put(particle)) {
throw new Error("Result expected for put call...");
}
// Add 1 to particle count:
this._count++;
return particle;
}
/**
* Removes the root SubPocket.
* @dev This function is designed for compatibility with `SubPocket.remove(...)`.
* @param sp The SubPocket that requested to be removed
* @return True if the sub pocket was found and removed, false otherwise.
*/
remove(sp) {
if (sp == this._root) {
this._root = undefined;
return true;
}
else {
return false;
}
}
/**
* Returns an array of all particles that exist wholly or partially within the given radius of the desired coordinates.
* @param radius The radius of the search
* @param center The 2D or 3D Vector coordinates of the search
* @param transform The optional transform function to apply to the particles before checking their inclusion
* @return A set of particles that were found in the search zone.
*/
search(radius, center, transform) {
if (!center.z)
center.z = 0;
if (this._root) {
return this._root.search(radius, center, new Set(), transform);
}
else {
return new Set();
}
}
/**
* Returns the closest object to the given position.
* @param position The 2D or 3D vector coordinate of the position to search
* @param startRadius An optional parameter to start the search radius at an expected distance.
* @dev This function starts at a starting radius and continuously increases the search radius
* until it finds some particles. Once some particles are found, it calculates the closest one
* to the search position. It is recommended to set an custom starting search radius if you
* are working with a very large number of particles.
* @return The closest particle to the search position, or undefined if there are no particles
* in the pocket.
*/
closest(position, startRadius) {
if (!position.z)
position.z = 0;
if (this._root) {
if (!startRadius)
startRadius = this._root.radius / 100;
for (let r = startRadius; r < this._root.radius * 2; r *= 2) {
const pool = this.search(r, position);
if (pool.size > 0) {
let closest = undefined;
let dist = undefined;
for (const p of pool) {
const p_dist = Pocket.Tools.mag(Pocket.Tools.sub(p, position));
if (dist === undefined || p_dist < dist) {
closest = p;
dist = p_dist;
}
}
return closest;
}
}
}
return undefined;
}
/**
* Builds a set of all the particles in the pocket.
* @dev This is not an optimized method. If you frequently need to work with an array of all
* particles in the pocket, it is recommended to maintain an array of all particles seperate
* from the pocket's implementation.
* @return A set of all particles in the pocket.
*/
all() {
if (!this._root)
return new Set();
return this.search(this._root.radius, this._root.position);
}
/**
* `INTERNAL USE ONLY.` Subtracts one from the particle count.
* @dev Do not call this function directly, it is automatically called when a particle is
* retrieved from the pocket.
*/
__particleRemoved() {
this._count--;
}
/**
* Getter for the pocket's particle count.
* @return The number of particles in this pocket.
*/
get count() {
return this._count;
}
}
Pocket.Tools = {
MAGIC_RATIO: 2,
/**
* Adds v0 to v1 and returns the new resulting vector.
* @param v0 Vector 0
* @param v1 Vector 1
* @returns Vector(v0 + v1)
*/
add: (v0, v1) => {
return {
x: v0.x + v1.x,
y: v0.y + v1.y,
z: (v0.z || 0) + (v1.z || 0)
};
},
/**
* Subracts v1 from v0 and returns the new resulting vector.
* @param v0 Vector 0
* @param v1 Vector 1
* @returns Vector(v0 - v1)
*/
sub: (v0, v1) => {
return {
x: v0.x - v1.x,
y: v0.y - v1.y,
z: (v0.z || 0) - (v1.z || 0)
};
},
/**
* Calculates the magnitude of a vector.
* @param v The vector to get the magnitude of
* @returns The magnitude of 'v'
*/
mag: (v) => {
return Math.sqrt(v.x * v.x + v.y * v.y + (v.z || 0) * (v.z || 0));
},
/**
* Calculates the product of vector 'v' and scalar product 'a' and returns the new resulting vector.
* @param v Vector to multiply
* @param a Scalar to multiply by
* @returns The product of v * a
*/
mul: (v, a) => {
return {
x: v.x * a,
y: v.y * a,
z: (v.z || 0) * a
};
},
/**
* Calculates the commutative product of two vectors and returns the new resulting vector.
* @param v0 Vector 0
* @param v1 Vector 1
* @returns The commutative product of 'v0' and 'v1'
*/
mulComm: (v0, v1) => {
return {
x: v0.x * v1.x,
y: v0.y * v1.y,
z: (v0.z || 0) * (v1.z || 0)
};
}
};