diff --git a/.gitignore b/.gitignore index c730ec13..1281a744 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,8 @@ /dist /coverage /playwright-report -/test-results \ No newline at end of file +/test-results +/_bmad +/_bmad-output +.github/agents/bmad-* +.github/prompts/bmad-* \ No newline at end of file diff --git a/public/main.js b/public/main.js index c0ac9d11..b9dfd5f6 100644 --- a/public/main.js +++ b/public/main.js @@ -1141,7 +1141,6 @@ function reGraph() { pack.cells = packCells; pack.cells.p = newCells.p; pack.cells.g = createTypedArray({maxValue: grid.points.length, from: newCells.g}); - pack.cells.q = d3.quadtree(newCells.p.map(([x, y], i) => [x, y, i])); pack.cells.h = createTypedArray({maxValue: 100, from: newCells.h}); pack.cells.area = createTypedArray({maxValue: UINT16_MAX, length: packCells.i.length}).map((_, cellId) => { const area = Math.abs(d3.polygonArea(getPackPolygon(cellId))); @@ -1234,7 +1233,7 @@ function showStatistics() { INFO && console.info(stats); // Dispatch event for test automation and external integrations - window.dispatchEvent(new CustomEvent('map:generated', { detail: { seed, mapId } })); + window.dispatchEvent(new CustomEvent("map:generated", {detail: {seed, mapId}})); } const regenerateMap = debounce(async function (options) { diff --git a/public/modules/dynamic/heightmap-selection.js b/public/modules/dynamic/heightmap-selection.js index 895719ae..3c1fe962 100644 --- a/public/modules/dynamic/heightmap-selection.js +++ b/public/modules/dynamic/heightmap-selection.js @@ -260,7 +260,7 @@ function getName(id) { } function getGraph(currentGraph) { - const newGraph = shouldRegenerateGrid(currentGraph, seed) ? generateGrid() : deepCopy(currentGraph); + const newGraph = shouldRegenerateGrid(currentGraph, seed) ? generateGrid() : structuredClone(currentGraph); delete newGraph.cells.h; return newGraph; } diff --git a/public/modules/resample.js b/public/modules/resample.js index b64dde1f..85c6072f 100644 --- a/public/modules/resample.js +++ b/public/modules/resample.js @@ -9,7 +9,7 @@ window.Resample = (function () { scale: Number */ function process({projection, inverse, scale}) { - const parentMap = {grid: deepCopy(grid), pack: deepCopy(pack), notes: deepCopy(notes)}; + const parentMap = {grid: structuredClone(grid), pack: structuredClone(pack), notes: structuredClone(notes)}; const riversData = saveRiversData(pack.rivers); grid = generateGrid(); @@ -28,7 +28,7 @@ window.Resample = (function () { reGraph(); Features.markupPack(); - Ice.generate() + Ice.generate(); createDefaultRuler(); restoreCellData(parentMap, inverse, scale); @@ -51,9 +51,10 @@ window.Resample = (function () { grid.cells.temp = new Int8Array(grid.points.length); grid.cells.prec = new Uint8Array(grid.points.length); + const parentPackQ = d3.quadtree(parentMap.pack.cells.p.map(([x, y], i) => [x, y, i])); grid.points.forEach(([x, y], newGridCell) => { const [parentX, parentY] = inverse(x, y); - const parentPackCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2]; + const parentPackCell = parentPackQ.find(parentX, parentY, Infinity)[2]; const parentGridCell = parentMap.pack.cells.g[parentPackCell]; grid.cells.h[newGridCell] = parentMap.grid.cells.h[parentGridCell]; @@ -347,11 +348,12 @@ window.Resample = (function () { } function restoreFeatureDetails(parentMap, inverse) { + const parentPackQ = d3.quadtree(parentMap.pack.cells.p.map(([x, y], i) => [x, y, i])); pack.features.forEach(feature => { if (!feature) return; const [x, y] = pack.cells.p[feature.firstCell]; const [parentX, parentY] = inverse(x, y); - const parentCell = parentMap.pack.cells.q.find(parentX, parentY, Infinity)[2]; + const parentCell = parentPackQ.find(parentX, parentY, Infinity)[2]; if (parentCell === undefined) return; const parentFeature = parentMap.pack.features[parentMap.pack.cells.f[parentCell]]; diff --git a/public/modules/submap.js b/public/modules/submap.js index 912daafb..8d23498e 100644 --- a/public/modules/submap.js +++ b/public/modules/submap.js @@ -57,7 +57,8 @@ window.Submap = (function () { const oldGrid = parentMap.grid; // build cache old -> [newcelllist] const forwardGridMap = parentMap.grid.points.map(_ => []); - resampler(grid.points, parentMap.pack.cells.q, (id, oldid) => { + const parentPackQ = d3.quadtree(parentMap.pack.cells.p.map(([x, y], i) => [x, y, i])); + resampler(grid.points, parentPackQ, (id, oldid) => { const cid = parentMap.pack.cells.g[oldid]; grid.cells.h[id] = oldGrid.cells.h[cid]; grid.cells.temp[id] = oldGrid.cells.temp[cid]; @@ -154,7 +155,7 @@ window.Submap = (function () { // find replacement: closest water cell const [ox, oy] = cells.p[id]; const [tx, ty] = inverse(x, y); - oldid = oldCells.q.find(tx, ty, Infinity)[2]; + oldid = d3.quadtree(oldCells.p.map(([px, py], i) => [px, py, i])).find(tx, ty, Infinity)[2]; if (!oldid) { console.warn("Warning, no id found in quad", id, "parent", gridCellId); continue; diff --git a/src/utils/arrayUtils.ts b/src/utils/arrayUtils.ts index add587d8..56051bfa 100644 --- a/src/utils/arrayUtils.ts +++ b/src/utils/arrayUtils.ts @@ -16,46 +16,6 @@ export const unique = (array: T[]): T[] => { return [...new Set(array)]; }; -/** - * Deep copy an object or array - * @param {Object|Array} obj - The object or array to deep copy - * @returns A deep copy of the object or array - */ -export const deepCopy = (obj: T): T => { - const id = (x: T): T => x; - const dcTArray = (a: T[]): T[] => a.map(id); - const dcObject = (x: object): object => - Object.fromEntries(Object.entries(x).map(([k, d]) => [k, dcAny(d)])); - const dcAny = (x: any): any => - x instanceof Object ? (cf.get(x.constructor) || id)(x) : x; - // don't map keys, probably this is what we would expect - const dcMapCore = (m: Map): [any, any][] => - [...m.entries()].map(([k, v]) => [k, dcAny(v)]); - - const cf: Map any> = new Map any>([ - [Int8Array, dcTArray], - [Uint8Array, dcTArray], - [Uint8ClampedArray, dcTArray], - [Int16Array, dcTArray], - [Uint16Array, dcTArray], - [Int32Array, dcTArray], - [Uint32Array, dcTArray], - [Float32Array, dcTArray], - [Float64Array, dcTArray], - [BigInt64Array, dcTArray], - [BigUint64Array, dcTArray], - [Map, (m) => new Map(dcMapCore(m))], - [WeakMap, (m) => new WeakMap(dcMapCore(m))], - [Array, (a) => a.map(dcAny)], - [Set, (s) => [...s.values()].map(dcAny)], - [Date, (d) => new Date(d.getTime())], - [Object, dcObject], - // ... extend here to implement their custom deep copy - ]); - - return dcAny(obj); -}; - /** * Get the appropriate typed array constructor based on the maximum value * @param {number} maxValue - The maximum value that will be stored in the array @@ -109,7 +69,6 @@ declare global { interface Window { last: typeof last; unique: typeof unique; - deepCopy: typeof deepCopy; getTypedArray: typeof getTypedArray; createTypedArray: typeof createTypedArray; INT8_MAX: number; diff --git a/src/utils/commonUtils.ts b/src/utils/commonUtils.ts index 6808eb11..dea115bd 100644 --- a/src/utils/commonUtils.ts +++ b/src/utils/commonUtils.ts @@ -67,7 +67,7 @@ export const getSegmentId = ( }; /** - * Creates a debounced function that delays invoking func until after ms milliseconds have elapsed + * Creates a debounced function that delays next func call until after ms milliseconds * @param func - The function to debounce * @param ms - The number of milliseconds to delay * @returns The debounced function @@ -212,11 +212,14 @@ export const isCtrlClick = (event: MouseEvent | KeyboardEvent): boolean => { * @returns Formatted date string */ export const generateDate = (from: number = 100, to: number = 1000): string => { - return new Date(rand(from, to), rand(12), rand(31)).toLocaleDateString("en", { - year: "numeric", - month: "long", - day: "numeric", - }); + return new Date(rand(from, to), rand(11), rand(1, 28)).toLocaleDateString( + "en", + { + year: "numeric", + month: "long", + day: "numeric", + }, + ); }; /** diff --git a/src/utils/graphUtils.ts b/src/utils/graphUtils.ts index 9b241780..9b297b41 100644 --- a/src/utils/graphUtils.ts +++ b/src/utils/graphUtils.ts @@ -1,5 +1,5 @@ import Alea from "alea"; -import { color } from "d3"; +import { color, quadtree } from "d3"; import Delaunator from "delaunator"; import { type Cells, @@ -266,6 +266,11 @@ export const findGridAll = ( return found; }; +const quadtreeCache = new WeakMap< + object, + ReturnType> +>(); + /** * Returns the index of the packed cell containing the given x and y coordinates * @param {number} x - The x coordinate @@ -277,10 +282,16 @@ export const findClosestCell = ( x: number, y: number, radius = Infinity, - packedGraph: any, + pack: { cells: { p: [number, number][] } }, ): number | undefined => { - if (!packedGraph.cells?.q) return; - const found = packedGraph.cells.q.find(x, y, radius); + if (!pack.cells?.p) throw new Error("Pack cells not found"); + let qTree = quadtreeCache.get(pack.cells.p); + if (!qTree) { + qTree = quadtree(pack.cells.p.map(([px, py], i) => [px, py, i])); + if (!qTree) throw new Error("Failed to create quadtree"); + quadtreeCache.set(pack.cells.p, qTree); + } + const found = qTree.find(x, y, radius); return found ? found[2] : undefined; }; @@ -414,8 +425,13 @@ export const findAllCellsInRadius = ( radius: number, packedGraph: any, ): number[] => { - // Use findAllInQuadtree directly instead of relying on prototype extension - const found = findAllInQuadtree(x, y, radius, packedGraph.cells.q); + const q = quadtree<[number, number, number]>( + packedGraph.cells.p.map( + ([px, py]: [number, number], i: number) => + [px, py, i] as [number, number, number], + ), + ); + const found = findAllInQuadtree(x, y, radius, q); return found.map((r: any) => r[2]); }; diff --git a/src/utils/index.ts b/src/utils/index.ts index 59b4b528..cd5581f8 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -26,7 +26,6 @@ window.list = list; import { createTypedArray, - deepCopy, getTypedArray, last, TYPED_ARRAY_MAX_VALUES, @@ -35,7 +34,6 @@ import { window.last = last; window.unique = unique; -window.deepCopy = deepCopy; window.getTypedArray = getTypedArray; window.createTypedArray = createTypedArray; window.INT8_MAX = TYPED_ARRAY_MAX_VALUES.INT8_MAX; @@ -274,90 +272,89 @@ window.drawPoint = drawPoint; window.drawPath = drawPath; export { - rn, - lim, - minmax, - normalize, - lerp, - isVowel, - trimVowels, - getAdjective, - nth, abbreviate, - list, - last, - unique, - deepCopy, - getTypedArray, - createTypedArray, - TYPED_ARRAY_MAX_VALUES, - rand, - P, - each, - gauss, - Pint, biased, - generateSeed, - getNumberInRange, - ra, - rw, - convertTemperature, - si, - getIntegerFromSI, - toHEX, - getColors, - getRandomColor, - getMixedColor, - C_12, - getComposedPath, - getNextId, - rollups, - distanceSquared, - getIsolines, - getPolesOfInaccessibility, - connectVertices, - findPath, - getVertexPath, - round, - capitalize, - splitInTwo, - parseTransform, - isValidJSON, - safeParseJSON, - sanitizeId, byId, - shouldRegenerateGrid, - generateGrid, - findGridAll, - findGridCell, - findClosestCell, + C_12, calculateVoronoi, - findAllCellsInRadius, - getPackPolygon, - getGridPolygon, - poissonDiscSampler, - isLand, - isWater, - findAllInQuadtree, - drawHeights, + capitalize, clipPoly, - getSegmentId, + connectVertices, + convertTemperature, + createTypedArray, debounce, - throttle, - parseError, - getBase64, - openURL, - wiki, - link, - isCtrlClick, - generateDate, - getLongitude, - getLatitude, - getCoordinates, - initializePrompt, + distanceSquared, drawCellsValue, + drawHeights, + drawPath, + drawPoint, drawPolygons, drawRouteConnections, - drawPoint, - drawPath, + each, + findAllCellsInRadius, + findAllInQuadtree, + findClosestCell, + findGridAll, + findGridCell, + findPath, + gauss, + generateDate, + generateGrid, + generateSeed, + getAdjective, + getBase64, + getColors, + getComposedPath, + getCoordinates, + getGridPolygon, + getIntegerFromSI, + getIsolines, + getLatitude, + getLongitude, + getMixedColor, + getNextId, + getNumberInRange, + getPackPolygon, + getPolesOfInaccessibility, + getRandomColor, + getSegmentId, + getTypedArray, + getVertexPath, + initializePrompt, + isCtrlClick, + isLand, + isValidJSON, + isVowel, + isWater, + last, + lerp, + lim, + link, + list, + minmax, + normalize, + nth, + openURL, + P, + parseError, + parseTransform, + Pint, + poissonDiscSampler, + ra, + rand, + rn, + rollups, + round, + rw, + safeParseJSON, + sanitizeId, + shouldRegenerateGrid, + si, + splitInTwo, + throttle, + toHEX, + trimVowels, + TYPED_ARRAY_MAX_VALUES, + unique, + wiki, };