fix: use structuredClone to avoid shared references in cloneSceneGraph
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.
This commit is contained in:
@@ -16,25 +16,6 @@ function extractIdPrefix(id: string): string {
|
|||||||
return underscoreIndex === -1 ? 'node' : id.slice(0, underscoreIndex)
|
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
|
* Deep clones a scene graph with all node IDs regenerated while preserving
|
||||||
* parent-child relationships and other internal references.
|
* parent-child relationships and other internal references.
|
||||||
@@ -56,12 +37,13 @@ export function cloneSceneGraph(sceneGraph: SceneGraph): SceneGraph {
|
|||||||
idMap.set(nodeId, generateId(prefix))
|
idMap.set(nodeId, generateId(prefix))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 2: Clone nodes with remapped references
|
// Pass 2: Deep clone nodes with remapped references
|
||||||
const clonedNodes = {} as Record<AnyNodeId, AnyNode>
|
const clonedNodes = {} as Record<AnyNodeId, AnyNode>
|
||||||
|
|
||||||
for (const [oldId, node] of Object.entries(nodes)) {
|
for (const [oldId, node] of Object.entries(nodes)) {
|
||||||
const newId = idMap.get(oldId)! as AnyNodeId
|
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
|
// Remap parentId
|
||||||
if (clonedNode.parentId && typeof clonedNode.parentId === 'string') {
|
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)
|
// Remap children array (walls, levels, buildings, sites, items can have children)
|
||||||
if ('children' in clonedNode && Array.isArray(clonedNode.children)) {
|
if ('children' in clonedNode && Array.isArray(clonedNode.children)) {
|
||||||
;(clonedNode as Record<string, unknown>).children = clonedNode.children
|
;(clonedNode as Record<string, unknown>).children = (
|
||||||
.map((childRef) => {
|
clonedNode.children as string[]
|
||||||
const childId = resolveChildRefId(childRef)
|
)
|
||||||
return childId ? idMap.get(childId) : undefined
|
.map((childId) => idMap.get(childId))
|
||||||
})
|
.filter((id): id is string => id !== undefined)
|
||||||
.filter((id): id is string => id !== undefined) as string[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remap wallId (items/doors/windows attached to walls)
|
// Remap wallId (items/doors/windows attached to walls)
|
||||||
|
|||||||
Reference in New Issue
Block a user