diff --git a/public/modules/dynamic/heightmap-selection.js b/public/modules/dynamic/heightmap-selection.js index 3c1fe962d..5a4be8f81 100644 --- a/public/modules/dynamic/heightmap-selection.js +++ b/public/modules/dynamic/heightmap-selection.js @@ -260,7 +260,14 @@ function getName(id) { } function getGraph(currentGraph) { - const newGraph = shouldRegenerateGrid(currentGraph, seed) ? generateGrid() : structuredClone(currentGraph); + if (shouldRegenerateGrid(currentGraph, seed)) return generateGrid(); + + // Deep clone avoiding non-cloneable properties (like D3 quadtrees with functions) + const newGraph = JSON.parse(JSON.stringify(currentGraph)); + // Recreate voronoi cells since JSON doesn't preserve typed arrays or circular refs + const {cells, vertices} = calculateVoronoi(newGraph.points, newGraph.boundary); + newGraph.cells = cells; + newGraph.vertices = vertices; delete newGraph.cells.h; return newGraph; } diff --git a/src/modules/resample.ts b/src/modules/resample.ts index a5e31c0fc..9b78fd2e0 100644 --- a/src/modules/resample.ts +++ b/src/modules/resample.ts @@ -13,6 +13,48 @@ import { import type { River } from "./river-generator"; import type { Point } from "./voronoi"; +/** + * Deeply clones an object, omitting properties that cannot be cloned with structuredClone. + * D3 quadtrees store accessor functions that fail structuredClone, so they're excluded. + */ +const safeDeepClone = (obj: T): T => { + if (obj === null || typeof obj !== "object") { + return obj; + } + + // Handle typed arrays - slice() creates a copy with a new underlying buffer + if (ArrayBuffer.isView(obj) && !(obj instanceof DataView)) { + return (obj as any).slice() as T; + } + + // Handle arrays + if (Array.isArray(obj)) { + return obj.map((item) => safeDeepClone(item)) as T; + } + + // Handle objects - skip quadtree (has functions) and other non-clonable properties + const cloned: Record = {}; + for (const key in obj) { + if (!Object.prototype.hasOwnProperty.call(obj, key)) continue; + + const value = (obj as Record)[key]; + + // Skip quadtree properties (D3 quadtrees have _x and _y functions) + if (value && typeof value === "object" && "_x" in (value as object)) { + continue; + } + + // Skip functions + if (typeof value === "function") { + continue; + } + + cloned[key] = safeDeepClone(value); + } + + return cloned as T; +}; + declare global { var Resample: Resampler; } @@ -495,9 +537,9 @@ class Resampler { process(options: ResamplerProcessOptions): void { const { projection, inverse, scale } = options; const parentMap = { - grid: structuredClone(grid), - pack: structuredClone(pack), - notes: structuredClone(notes), + grid: safeDeepClone(grid), + pack: safeDeepClone(pack), + notes: safeDeepClone(notes), }; const riversData = this.saveRiversData(pack.rivers); diff --git a/src/types/PackedGraph.ts b/src/types/PackedGraph.ts index df84be863..b07ab1d86 100644 --- a/src/types/PackedGraph.ts +++ b/src/types/PackedGraph.ts @@ -25,7 +25,7 @@ export interface PackedGraph { p: [number, number][]; // cell polygon points b: boolean[]; // cell is on border h: TypedArray; // cell heights - q: Quadtree<[number, number, number]>; // cell quadtree index + q?: Quadtree<[number, number, number]>; // cell quadtree index (optional, not cloned) /** Terrain type */ t: TypedArray; // cell terrain types r: TypedArray; // river id passing through cell