Skip to content

fix: improve deep cloning for graph and resampling to handle non-cloneable properties#1365

Open
SheepFromHeaven wants to merge 1 commit intoAzgaar:masterfrom
SheepFromHeaven:fix/transform-issue
Open

fix: improve deep cloning for graph and resampling to handle non-cloneable properties#1365
SheepFromHeaven wants to merge 1 commit intoAzgaar:masterfrom
SheepFromHeaven:fix/transform-issue

Conversation

@SheepFromHeaven
Copy link
Copy Markdown
Collaborator

Description

@SheepFromHeaven SheepFromHeaven self-assigned this Mar 26, 2026
Copilot AI review requested due to automatic review settings March 26, 2026 09:52
@netlify
Copy link
Copy Markdown

netlify bot commented Mar 26, 2026

Deploy Preview for afmg ready!

Name Link
🔨 Latest commit b80dc94
🔍 Latest deploy log https://app.netlify.com/projects/afmg/deploys/69c501d375a74e000951749a
😎 Deploy Preview https://deploy-preview-1365--afmg.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates map/graph cloning behavior to avoid failures when cloning objects that contain non-cloneable properties (notably D3 quadtree accessor functions), especially during resampling and heightmap template preview generation.

Changes:

  • Make PackedGraph.cells.q optional to reflect that the quadtree index is transient / not safely cloneable.
  • Replace structuredClone usage in resampling with a custom safeDeepClone that skips non-cloneable members.
  • Update heightmap-selection graph cloning logic to avoid structuredClone and reconstruct Voronoi-derived structures.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/types/PackedGraph.ts Marks the quadtree index as optional to indicate it may not be present after cloning.
src/modules/resample.ts Introduces safeDeepClone and uses it to clone grid, pack, and notes before resampling.
public/modules/dynamic/heightmap-selection.js Changes graph cloning strategy for template previews and rebuilds Voronoi cells/vertices post-clone.

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)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making PackedGraph.cells.q optional is a type-level breaking change for any code that expects cells.q to always exist. If q is only non-cloneable transient state, consider modeling it outside of PackedGraph (or as a separate runtime-only augmentation type) so consumers of PackedGraph don't need to handle undefined everywhere.

Copilot uses AI. Check for mistakes.
Comment on lines +265 to 272
// 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;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using JSON.parse(JSON.stringify(currentGraph)) as a deep clone is fragile and can be very expensive for large graphs. It also drops/rewrites non-JSON-safe values (typed arrays, undefined, Infinity/NaN, etc.), and will still throw if any circular references exist. Consider cloning only the specific fields needed for preview generation (e.g., points/boundary/dimensions), or using a dedicated clone that explicitly omits the non-cloneable quadtree rather than serializing the entire graph.

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +56
const safeDeepClone = <T>(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<string, unknown> = {};
for (const key in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;

const value = (obj as Record<string, unknown>)[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;
};
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

safeDeepClone creates plain objects via {} and recursively copies enumerable own props, which does not preserve prototypes (e.g., Quadtree instances, classes) and may change behavior if any cloned values rely on their prototype methods. If the intent is only to omit pack.cells.q, consider explicitly stripping that field (or using structuredClone with a targeted fallback) so other object types keep their identity/shape as much as possible.

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +43
// Skip quadtree properties (D3 quadtrees have _x and _y functions)
if (value && typeof value === "object" && "_x" in (value as object)) {
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The quadtree-detection heuristic ("_x" in value) is fairly broad and can skip cloning for any object that happens to have an _x property, even if it is not a D3 quadtree. If you only need to skip the known cells.q quadtree field, prefer an explicit key/path check (e.g., when key === "q" under cells) to avoid unintentionally dropping unrelated data.

Suggested change
// Skip quadtree properties (D3 quadtrees have _x and _y functions)
if (value && typeof value === "object" && "_x" in (value as object)) {
// Skip known quadtree properties (D3 quadtrees have _x and _y functions)
if (key === "q" && value && typeof value === "object" && "_x" in (value as object)) {

Copilot uses AI. Check for mistakes.
@Azgaar
Copy link
Copy Markdown
Owner

Azgaar commented Mar 26, 2026

Hi @SheepFromHeaven, are you sure we need it? There was a change to not store quadtree in data anymore. We can always recreate it when needed. So we don't need to do a weird deepclone of data.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants