From b223dc62da49e82f4d176e4aa8d3e3917cda8e7b Mon Sep 17 00:00:00 2001 From: Marc Emmanuel Date: Thu, 22 Jan 2026 17:24:02 +0100 Subject: [PATCH] fix: implement quadtree search for points within a radius (#1274) --- src/utils/graphUtils.ts | 180 ++++++++++++++++++++++------------------ 1 file changed, 97 insertions(+), 83 deletions(-) diff --git a/src/utils/graphUtils.ts b/src/utils/graphUtils.ts index 875445fb..274d69f9 100644 --- a/src/utils/graphUtils.ts +++ b/src/utils/graphUtils.ts @@ -206,13 +206,109 @@ export const findClosestCell = (x: number, y: number, radius = Infinity, packedG return found ? found[2] : undefined; } +/** + * Searches a quadtree for all points within a given radius + * Based on https://bl.ocks.org/lwthatcher/b41479725e0ff2277c7ac90df2de2b5e + * @param {number} x - The x coordinate of the search center + * @param {number} y - The y coordinate of the search center + * @param {number} radius - The search radius + * @param {Object} quadtree - The D3 quadtree to search + * @returns {Array} - An array of found data points within the radius + */ +export const findAllInQuadtree = (x: number, y: number, radius: number, quadtree: any) => { + const radiusSearchInit = (t: any, radius: number) => { + t.result = []; + (t.x0 = t.x - radius), (t.y0 = t.y - radius); + (t.x3 = t.x + radius), (t.y3 = t.y + radius); + t.radius = radius * radius; + }; + + const radiusSearchVisit = (t: any, d2: number) => { + t.node.data.scanned = true; + if (d2 < t.radius) { + do { + t.result.push(t.node.data); + t.node.data.selected = true; + } while ((t.node = t.node.next)); + } + }; + + class Quad { + node: any; + x0: number; + y0: number; + x1: number; + y1: number; + constructor(node: any, x0: number, y0: number, x1: number, y1: number) { + this.node = node; + this.x0 = x0; + this.y0 = y0; + this.x1 = x1; + this.y1 = y1; + } + } + + const t: any = {x, y, x0: quadtree._x0, y0: quadtree._y0, x3: quadtree._x1, y3: quadtree._y1, quads: [], node: quadtree._root}; + if (t.node) t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3)); + radiusSearchInit(t, radius); + + var i = 0; + while ((t.q = t.quads.pop())) { + i++; + + // Stop searching if this quadrant can't contain a closer node. + if ( + !(t.node = t.q.node) || + (t.x1 = t.q.x0) > t.x3 || + (t.y1 = t.q.y0) > t.y3 || + (t.x2 = t.q.x1) < t.x0 || + (t.y2 = t.q.y1) < t.y0 + ) + continue; + + // Bisect the current quadrant. + if (t.node.length) { + t.node.explored = true; + var xm: number = (t.x1 + t.x2) / 2, + ym: number = (t.y1 + t.y2) / 2; + + t.quads.push( + new Quad(t.node[3], xm, ym, t.x2, t.y2), + new Quad(t.node[2], t.x1, ym, xm, t.y2), + new Quad(t.node[1], xm, t.y1, t.x2, ym), + new Quad(t.node[0], t.x1, t.y1, xm, ym) + ); + + // Visit the closest quadrant first. + if ((t.i = (+(y >= ym) << 1) | +(x >= xm))) { + t.q = t.quads[t.quads.length - 1]; + t.quads[t.quads.length - 1] = t.quads[t.quads.length - 1 - t.i]; + t.quads[t.quads.length - 1 - t.i] = t.q; + } + } + + // Visit this point. (Visiting coincident points isn't necessary!) + else { + var dx = x - +quadtree._x.call(null, t.node.data), + dy = y - +quadtree._y.call(null, t.node.data), + d2 = dx * dx + dy * dy; + radiusSearchVisit(t, d2); + } + } + return t.result; +} + /** * Returns an array of packed cell indexes within a specified radius from given x and y coordinates * @param {number} x - The x coordinate * @param {number} y - The y coordinate + * @param {number} radius - The search radius + * @param {Object} packedGraph - The packed graph containing cells with quadtree + * @returns {number[]} - An array of cell indexes within the radius */ export const findAllCellsInRadius = (x: number, y: number, radius: number, packedGraph: any): number[] => { - const found = packedGraph.cells.q.findAll(x, y, radius); + // Use findAllInQuadtree directly instead of relying on prototype extension + const found = findAllInQuadtree(x, y, radius, packedGraph.cells.q); return found.map((r: any) => r[2]); } @@ -325,88 +421,6 @@ export const isWater = (i: number, packedGraph: any) => { return packedGraph.cells.h[i] < 20; } -export const findAllInQuadtree = (x: number, y: number, radius: number, quadtree: any) => { - const radiusSearchInit = (t: any, radius: number) => { - t.result = []; - (t.x0 = t.x - radius), (t.y0 = t.y - radius); - (t.x3 = t.x + radius), (t.y3 = t.y + radius); - t.radius = radius * radius; - }; - - const radiusSearchVisit = (t: any, d2: number) => { - t.node.data.scanned = true; - if (d2 < t.radius) { - do { - t.result.push(t.node.data); - t.node.data.selected = true; - } while ((t.node = t.node.next)); - } - }; - - class Quad { - node: any; - x0: number; - y0: number; - x1: number; - y1: number; - constructor(node: any, x0: number, y0: number, x1: number, y1: number) { - this.node = node; - this.x0 = x0; - this.y0 = y0; - this.x1 = x1; - this.y1 = y1; - } - } - - const t: any = {x, y, x0: quadtree._x0, y0: quadtree._y0, x3: quadtree._x1, y3: quadtree._y1, quads: [], node: quadtree._root}; - if (t.node) t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3)); - radiusSearchInit(t, radius); - - var i = 0; - while ((t.q = t.quads.pop())) { - i++; - - // Stop searching if this quadrant can’t contain a closer node. - if ( - !(t.node = t.q.node) || - (t.x1 = t.q.x0) > t.x3 || - (t.y1 = t.q.y0) > t.y3 || - (t.x2 = t.q.x1) < t.x0 || - (t.y2 = t.q.y1) < t.y0 - ) - continue; - - // Bisect the current quadrant. - if (t.node.length) { - t.node.explored = true; - var xm: number = (t.x1 + t.x2) / 2, - ym: number = (t.y1 + t.y2) / 2; - - t.quads.push( - new Quad(t.node[3], xm, ym, t.x2, t.y2), - new Quad(t.node[2], t.x1, ym, xm, t.y2), - new Quad(t.node[1], xm, t.y1, t.x2, ym), - new Quad(t.node[0], t.x1, t.y1, xm, ym) - ); - - // Visit the closest quadrant first. - if ((t.i = (+(y >= ym) << 1) | +(x >= xm))) { - t.q = t.quads[t.quads.length - 1]; - t.quads[t.quads.length - 1] = t.quads[t.quads.length - 1 - t.i]; - t.quads[t.quads.length - 1 - t.i] = t.q; - } - } - - // Visit this point. (Visiting coincident points isn’t necessary!) - else { - var dx = x - +quadtree._x.call(null, t.node.data), - dy = y - +quadtree._y.call(null, t.node.data), - d2 = dx * dx + dy * dy; - radiusSearchVisit(t, d2); - } - } - return t.result; -} // draw raster heightmap preview (not used in main generation) /**