UNPKG

13 kBPlain TextView Raw
1import type { NodeGeometry, Indexable } from './types';
2import type { Rectangle } from './Rectangle';
3import type { Circle } from './Circle';
4import type { Line } from './Line';
5
6/**
7 * Quadtree Constructor Properties
8 */
9export interface QuadtreeProps {
10
11 /**
12 * Width of the node.
13 */
14 width: number
15
16 /**
17 * Height of the node.
18 */
19 height: number
20
21 /**
22 * X Offset of the node.
23 * @defaultValue `0`
24 */
25 x?: number
26
27 /**
28 * Y Offset of the node.
29 * @defaultValue `0`
30 */
31 y?: number
32
33 /**
34 * Max objects this node can hold before it splits.
35 * @defaultValue `10`
36 */
37 maxObjects?: number
38
39 /**
40 * Total max nesting levels of the root Quadtree node.
41 * @defaultValue `4`
42 */
43 maxLevels?: number
44}
45
46/**
47 * Class representing a Quadtree node.
48 *
49 * @example
50 * ```typescript
51 * const tree = new Quadtree({
52 * width: 100,
53 * height: 100,
54 * x: 0, // optional, default: 0
55 * y: 0, // optional, default: 0
56 * maxObjects: 10, // optional, default: 10
57 * maxLevels: 4, // optional, default: 4
58 * });
59 * ```
60 *
61 * @example Typescript: If you like to be explicit, you optionally can pass in a generic type for objects to be stored in the Quadtree:
62 * ```typescript
63 * class GameEntity extends Rectangle {
64 * ...
65 * }
66 * const tree = new Quadtree<GameEntity>({
67 * width: 100,
68 * height: 100,
69 * });
70 * ```
71 */
72export class Quadtree<ObjectsType extends Rectangle|Circle|Line|Indexable> {
73
74 /**
75 * The numeric boundaries of this node.
76 * @readonly
77 */
78 bounds: NodeGeometry;
79
80 /**
81 * Max objects this node can hold before it splits.
82 * @defaultValue `10`
83 * @readonly
84 */
85 maxObjects: number;
86
87 /**
88 * Total max nesting levels of the root Quadtree node.
89 * @defaultValue `4`
90 * @readonly
91 */
92 maxLevels: number;
93
94 /**
95 * The level of this node.
96 * @defaultValue `0`
97 * @readonly
98 */
99 level: number;
100
101 /**
102 * Array of objects in this node.
103 * @defaultValue `[]`
104 * @readonly
105 */
106 objects: ObjectsType[];
107
108 /**
109 * Subnodes of this node
110 * @defaultValue `[]`
111 * @readonly
112 */
113 nodes: Quadtree<ObjectsType>[];
114
115 /**
116 * Quadtree Constructor
117 * @param props - bounds and properties of the node
118 * @param level - depth level (internal use only, required for subnodes)
119 */
120 constructor(props:QuadtreeProps, level=0) {
121
122 this.bounds = {
123 x: props.x || 0,
124 y: props.y || 0,
125 width: props.width,
126 height: props.height,
127 };
128 this.maxObjects = (typeof props.maxObjects === 'number') ? props.maxObjects : 10;
129 this.maxLevels = (typeof props.maxLevels === 'number') ? props.maxLevels : 4;
130 this.level = level;
131
132 this.objects = [];
133 this.nodes = [];
134 }
135
136 /**
137 * Get the quadrant (subnode indexes) an object belongs to.
138 *
139 * @example Mostly for internal use but you can call it like so:
140 * ```typescript
141 * const tree = new Quadtree({ width: 100, height: 100 });
142 * const rectangle = new Rectangle({ x: 25, y: 25, width: 10, height: 10 });
143 * const indexes = tree.getIndex(rectangle);
144 * console.log(indexes); // [1]
145 * ```
146 *
147 * @param obj - object to be checked
148 * @returns Array containing indexes of intersecting subnodes (0-3 = top-right, top-left, bottom-left, bottom-right).
149 */
150 getIndex(obj:Rectangle|Circle|Line|Indexable): number[] {
151 return obj.qtIndex(this.bounds);
152 }
153
154 /**
155 * Split the node into 4 subnodes.
156 * @internal Mostly for internal use! You should only call this yourself if you know what you are doing.
157 *
158 * @example Manual split:
159 * ```typescript
160 * const tree = new Quadtree({ width: 100, height: 100 });
161 * tree.split();
162 * console.log(tree); // now tree has four subnodes
163 * ```
164 */
165 split(): void {
166
167 const level = this.level + 1,
168 width = this.bounds.width/2,
169 height = this.bounds.height/2,
170 x = this.bounds.x,
171 y = this.bounds.y;
172
173 const coords = [
174 { x: x + width, y: y },
175 { x: x, y: y },
176 { x: x, y: y + height },
177 { x: x + width, y: y + height },
178 ];
179
180 for(let i=0; i < 4; i++) {
181 this.nodes[i] = new Quadtree({
182 x: coords[i].x,
183 y: coords[i].y,
184 width,
185 height,
186 maxObjects: this.maxObjects,
187 maxLevels: this.maxLevels,
188 }, level);
189 }
190 }
191
192
193 /**
194 * Insert an object into the node. If the node
195 * exceeds the capacity, it will split and add all
196 * objects to their corresponding subnodes.
197 *
198 * @example you can use any shape here (or object with a qtIndex method, see README):
199 * ```typescript
200 * const tree = new Quadtree({ width: 100, height: 100 });
201 * tree.insert(new Rectangle({ x: 25, y: 25, width: 10, height: 10, data: 'data' }));
202 * tree.insert(new Circle({ x: 25, y: 25, r: 10, data: 512 }));
203 * tree.insert(new Line({ x1: 25, y1: 25, x2: 60, y2: 40, data: { custom: 'property'} }));
204 * ```
205 *
206 * @param obj - Object to be added.
207 */
208 insert(obj:ObjectsType): void {
209
210 //if we have subnodes, call insert on matching subnodes
211 if(this.nodes.length) {
212 const indexes = this.getIndex(obj);
213
214 for(let i=0; i<indexes.length; i++) {
215 this.nodes[indexes[i]].insert(obj);
216 }
217 return;
218 }
219
220 //otherwise, store object here
221 this.objects.push(obj);
222
223 //maxObjects reached
224 if(this.objects.length > this.maxObjects && this.level < this.maxLevels) {
225
226 //split if we don't already have subnodes
227 if(!this.nodes.length) {
228 this.split();
229 }
230
231 //add all objects to their corresponding subnode
232 for(let i=0; i<this.objects.length; i++) {
233 const indexes = this.getIndex(this.objects[i]);
234 for(let k=0; k<indexes.length; k++) {
235 this.nodes[indexes[k]].insert(this.objects[i]);
236 }
237 }
238
239 //clean up this node
240 this.objects = [];
241 }
242 }
243
244
245 /**
246 * Return all objects that could collide with the given geometry.
247 *
248 * @example Just like insert, you can use any shape here (or object with a qtIndex method, see README):
249 * ```typescript
250 * tree.retrieve(new Rectangle({ x: 25, y: 25, width: 10, height: 10, data: 'data' }));
251 * tree.retrieve(new Circle({ x: 25, y: 25, r: 10, data: 512 }));
252 * tree.retrieve(new Line({ x1: 25, y1: 25, x2: 60, y2: 40, data: { custom: 'property'} }));
253 * ```
254 *
255 * @param obj - geometry to be checked
256 * @returns Array containing all detected objects.
257 */
258 retrieve(obj:Rectangle|Circle|Line|Indexable): ObjectsType[] {
259
260 const indexes = this.getIndex(obj);
261 let returnObjects = this.objects;
262
263 //if we have subnodes, retrieve their objects
264 if(this.nodes.length) {
265 for(let i=0; i<indexes.length; i++) {
266 returnObjects = returnObjects.concat(this.nodes[indexes[i]].retrieve(obj));
267 }
268 }
269
270 // remove duplicates
271 if (this.level === 0) {
272 return Array.from(new Set(returnObjects));
273 }
274
275 return returnObjects;
276 }
277
278
279 /**
280 * Remove an object from the tree.
281 * If you have to remove many objects, consider clearing the entire tree and rebuilding it or use the `fast` flag to cleanup after the last removal.
282 * @beta
283 *
284 * @example
285 * ```typescript
286 * const tree = new Quadtree({ width: 100, height: 100 });
287 * const circle = new Circle({ x: 25, y: 25, r: 10, data: 512 });
288 * tree.insert(circle);
289 * tree.remove(circle);
290 * ```
291 *
292 * @example Bulk fast removals and final cleanup:
293 * ```javascript
294 * const tree = new Quadtree({ width: 100, height: 100 });
295 * const rects = [];
296 * for(let i=0; i<20; i++) {
297 * rects[i] = new Rectangle({ x: 25, y: 25, width: 50, height: 50 });
298 * tree.insert(rects[i]);
299 * }
300 * for(let i=rects.length-1; i>0; i--) {
301 * //fast=true – just remove the object (may leaves vacant subnodes)
302 * //fast=false – cleanup empty subnodes (default)
303 * const fast = (i !== 0);
304 * tree.remove(rects[i], fast);
305 * }
306 * ```
307 *
308 * @param obj - Object to be removed.
309 * @param fast - Set to true to increase performance temporarily by preventing cleanup of empty subnodes (optional, default: false).
310 * @returns Weather or not the object was removed from THIS node (no recursive check).
311 */
312 remove(obj: ObjectsType, fast=false): boolean {
313
314 const indexOf = this.objects.indexOf(obj);
315
316 // remove objects
317 if(indexOf > -1) {
318 this.objects.splice(indexOf, 1);
319 }
320
321 // remove from all subnodes
322 for (let i = 0; i < this.nodes.length; i++) {
323 this.nodes[i].remove(obj);
324 }
325
326 // remove all empty subnodes
327 if(this.level === 0 && !fast) {
328 this.join();
329 }
330
331 return (indexOf !== -1);
332 }
333
334 /**
335 * Update an object already in the tree (shorthand for remove and insert).
336 * If you have to update many objects, consider clearing and rebuilding the
337 * entire tree or use the `fast` flag to cleanup after the last update.
338 * @beta
339 *
340 * @example
341 * ```typescript
342 * const tree = new Quadtree({ width: 100, height: 100, maxObjects: 1 });
343 * const rect1 = new Rectangle({ x: 25, y: 25, width: 10, height: 10 });
344 * const rect2 = new Rectangle({ x: 25, y: 25, width: 10, height: 10 });
345 * tree.insert(rect1);
346 * tree.insert(rect2);
347 * rect1.x = 75;
348 * rect1.y = 75;
349 * tree.update(rect1);
350 * ```
351 * @example Bulk fast update and final cleanup:
352 * ```javascript
353 * const tree = new Quadtree({ width: 100, height: 100 });
354 * const rects = [];
355 * for(let i=0; i<20; i++) {
356 * rects[i] = new Rectangle({ x: 20, y: 20, width: 20, height: 20 });
357 * tree.insert(rects[i]);
358 * }
359 * for(let i=rects.length-1; i>0; i--) {
360 * rects[i].x = 20 + Math.random()*60;
361 * rects[i].y = 20 + Math.random()*60;
362 * //fast=true – just re-insert the object (may leaves vacant subnodes)
363 * //fast=false – cleanup empty subnodes (default)
364 * const fast = (i !== 0);
365 * tree.update(rects[i], fast);
366 * }
367 * ```
368 *
369 * @param obj - Object to be updated.
370 * @param fast - Set to true to increase performance temporarily by preventing cleanup of empty subnodes (optional, default: false).
371 */
372 update(obj: ObjectsType, fast=false): void {
373 this.remove(obj, fast);
374 this.insert(obj);
375 }
376
377 /**
378 * The opposite of a split: try to merge and dissolve subnodes.
379 * @beta
380 * @internal Mostly for internal use! You should only call this yourself if you know what you are doing.
381 *
382 * @example Manual join:
383 * ```typescript
384 * const tree = new Quadtree({ width: 100, height: 100 });
385 * tree.split();
386 * console.log(tree.nodes.length); // 4
387 * tree.join();
388 * console.log(tree.nodes.length); // 0
389 * ```
390 *
391 * @returns The objects from this node and all subnodes combined.
392 */
393 join(): ObjectsType[] {
394
395 // recursive join
396 let allObjects = Array.from(this.objects);
397 for(let i=0; i < this.nodes.length; i++) {
398 const bla = this.nodes[i].join();
399 allObjects = allObjects.concat(bla);
400 }
401
402 // remove duplicates
403 const uniqueObjects = Array.from(new Set(allObjects));
404
405 if(uniqueObjects.length <= this.maxObjects) {
406 this.objects = uniqueObjects;
407 for(let i=0; i < this.nodes.length; i++) {
408 this.nodes[i].objects = [];
409 }
410 this.nodes = [];
411 }
412
413 return allObjects;
414 }
415
416 /**
417 * Clear the Quadtree.
418 *
419 * @example
420 * ```typescript
421 * const tree = new Quadtree({ width: 100, height: 100 });
422 * tree.insert(new Circle({ x: 25, y: 25, r: 10 }));
423 * tree.clear();
424 * console.log(tree); // tree.objects and tree.nodes are empty
425 * ```
426 */
427 clear(): void {
428
429 this.objects = [];
430
431 for(let i=0; i < this.nodes.length; i++) {
432 if(this.nodes.length) {
433 this.nodes[i].clear();
434 }
435 }
436
437 this.nodes = [];
438 }
439}
\No newline at end of file