From 3474ee226d9e84e63ca9fd4c7f2d152afe512be8 Mon Sep 17 00:00:00 2001 From: Pascal Date: Sat, 28 Mar 2026 22:45:33 +0000 Subject: [PATCH] fix: use structuredClone to avoid shared references in cloneSceneGraph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original implementation used spread ({ ...node }) which only shallow-copies. Nested objects (asset, polygon, position tuples, metadata) would share references between the original and cloned scene, meaning mutations to the clone could corrupt the original. Also removed resolveChildRefId helper — children arrays in the schema are always string IDs, so the object-with-id handling was dead code. --- packages/core/src/utils/clone-scene-graph.ts | 35 +++++--------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/packages/core/src/utils/clone-scene-graph.ts b/packages/core/src/utils/clone-scene-graph.ts index eac38192..2eb3ff39 100644 --- a/packages/core/src/utils/clone-scene-graph.ts +++ b/packages/core/src/utils/clone-scene-graph.ts @@ -16,25 +16,6 @@ function extractIdPrefix(id: string): string { return underscoreIndex === -1 ? 'node' : id.slice(0, underscoreIndex) } -/** - * Resolves a child reference to a node ID. - * Supports both string IDs and embedded child node objects with an `id` field. - */ -function resolveChildRefId(child: unknown): string | undefined { - if (typeof child === 'string') { - return child - } - - if (child && typeof child === 'object' && 'id' in child) { - const id = (child as { id?: unknown }).id - if (typeof id === 'string') { - return id - } - } - - return undefined -} - /** * Deep clones a scene graph with all node IDs regenerated while preserving * parent-child relationships and other internal references. @@ -56,12 +37,13 @@ export function cloneSceneGraph(sceneGraph: SceneGraph): SceneGraph { idMap.set(nodeId, generateId(prefix)) } - // Pass 2: Clone nodes with remapped references + // Pass 2: Deep clone nodes with remapped references const clonedNodes = {} as Record for (const [oldId, node] of Object.entries(nodes)) { const newId = idMap.get(oldId)! as AnyNodeId - const clonedNode = { ...node, id: newId } as AnyNode + // structuredClone to avoid shared references between original and clone + const clonedNode = structuredClone({ ...node, id: newId }) as AnyNode // Remap parentId if (clonedNode.parentId && typeof clonedNode.parentId === 'string') { @@ -70,12 +52,11 @@ export function cloneSceneGraph(sceneGraph: SceneGraph): SceneGraph { // Remap children array (walls, levels, buildings, sites, items can have children) if ('children' in clonedNode && Array.isArray(clonedNode.children)) { - ;(clonedNode as Record).children = clonedNode.children - .map((childRef) => { - const childId = resolveChildRefId(childRef) - return childId ? idMap.get(childId) : undefined - }) - .filter((id): id is string => id !== undefined) as string[] + ;(clonedNode as Record).children = ( + clonedNode.children as string[] + ) + .map((childId) => idMap.get(childId)) + .filter((id): id is string => id !== undefined) } // Remap wallId (items/doors/windows attached to walls)