-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpocket.ts
More file actions
596 lines (550 loc) · 17.1 KB
/
pocket.ts
File metadata and controls
596 lines (550 loc) · 17.1 KB
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
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
/*!
Pocket v2.0.0
(c) 2020 Trevor Richard
Released under the MIT License.
*/
/**
* Vector interface that supports 2D and optional 3D coordinates.
* @param x The x value of the vector
* @param y The y value of the vector
* @param z The optional z value of the vector. If this value is ommitted, it will be assumed to be zero.
*/
export interface Vector {
x: number
y: number
z?: number
}
/**
* A particle object that consists of both positional data and a custom data payload.
*/
export class Particle<T> implements Vector {
// Positional Data:
private _x: number
private _y: number
private _z: number
private _radius: number = 0
// Custom data payload:
public data!: T
// Metadata:
private _pocket?: Pocket<T>
private _subPocket?: SubPocket<T>
constructor({
x,
y,
z = 0,
radius = 0,
data,
}: {
x: number
y: number
z?: number
radius?: number
data?: T
}) {
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: Vector) {
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
}
}
/**
* Removes the particle from its pocket if it exists in one and returns this particle object.
* @return `this`
*/
retrieve() {
this._subPocket?.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: Pocket<T>) {
if (this._pocket && this._pocket != pocket)
throw {
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: SubPocket<T>) {
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: number) {
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: number) {
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: number) {
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: number) {
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<T> {
radius: number
parent: SubPocket<T> | Pocket<T>
pocket: Pocket<T>
subPockets: SubPocket<T>[]
particles: Particle<T>[]
position: Vector
constructor({
parent,
radius,
position,
}: {
parent: SubPocket<T> | Pocket<T>
radius: number
position: Vector
}) {
this.parent = parent
this.radius = radius
this.pocket = parent instanceof Pocket ? parent : parent.pocket
this.subPockets = new Array<SubPocket<T>>()
this.particles = new Array<Particle<T>>()
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 does not fit in this sub pocket.
*/
put(p: Particle<T>): SubPocket<T> | undefined {
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<T>({
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: Particle<T>): boolean {
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: SubPocket<T>): boolean {
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: number,
center: Vector,
set: Set<Particle<T>>,
transform?: (v: Vector) => Vector
) {
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<T> {
static 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: Vector, v1: Vector) => {
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: Vector, v1: Vector) => {
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: Vector) => {
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: Vector, a: number) => {
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: Vector, v1: Vector) => {
return {
x: v0.x * v1.x,
y: v0.y * v1.y,
z: (v0.z || 0) * (v1.z || 0),
}
},
}
private _root: SubPocket<T> | undefined
private _count: number
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: Particle<T>): Particle<T> {
// 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)) {
// Add 1 to particle count
this._count++
// Return the 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<T>({
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 the particle
return particle
}
/**
* Removes the root SubPocket.
* @dev THIS FUNCTION IS PRIMARILY FOR INTERNAL USE. If you are looking to remove a particle from the pocket,
* go to: `Particle.retrieve()`
* @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: SubPocket<T>): boolean {
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: number, center: Vector, transform?: (v: Vector) => Vector) {
if (!center.z) center.z = 0
if (this._root) {
return this._root.search(radius, center, new Set(), transform)
} else {
return new Set<Particle<T>>()
}
}
/**
* 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: Vector, startRadius?: number) {
if (!position.z) position.z = 0
if (this._root) {
if (!startRadius) startRadius = this._root.radius / 100
// Ensure the max radius will encompass the entire root pocket no matter where the position is located:
const maxRadius =
(Pocket.Tools.mag(Pocket.Tools.sub(this._root.position, position)) +
this._root.radius) *
2
for (let r = startRadius; r < maxRadius; r *= 2) {
const pool = this.search(r, position)
if (pool.size > 0) {
let closest: Particle<T> | undefined = undefined
let dist: number | undefined = 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 <Particle<T>>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<Particle<T>>()
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
}
}