1 | import type { NodeGeometry, Indexable } from './types';
|
2 | import type { Rectangle } from './Rectangle';
|
3 | import type { Circle } from './Circle';
|
4 | import type { Line } from './Line';
|
5 |
|
6 | /**
|
7 | * Quadtree Constructor Properties
|
8 | */
|
9 | export 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 | */
|
72 | export 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 |