Fix: Restore floor drag/delete functionality for imported legacy JSON… (#413)
* Fix: Restore floor drag/delete functionality for imported legacy JSON files Summary Fixed an issue where floors became impossible to drag or delete after importing JSON configuration files created in versions prior to 0.9.1. Root Cause The migration process was executing elevator parent migration before all level nodes had their children fully normalized. As a result, imported scenes could end up with inconsistent parent-child relationships, causing floor management operations such as dragging and deletion to fail. Changes Made Refactored migrateNodes() into a two-pass migration process. Added normalization for level nodes: Ensures level values are valid finite numbers. Removes references to missing child nodes. Preserves only valid children during migration. Moved elevator migration logic to a dedicated second pass: Elevator parent migration now runs only after all level.children relationships have been stabilized. Prevents invalid hierarchy reconstruction when importing legacy JSON files. Result Imported layouts from versions prior to 0.9.1 now correctly preserve floor hierarchy, allowing floors to be dragged, reordered, and deleted as expected. * Update use-scene.ts
This commit is contained in:
@@ -547,9 +547,18 @@ function migrateNodes(nodes: Record<string, any>): {
|
|||||||
// any per-type migration runs, so already-saved scenes load cleanly.
|
// any per-type migration runs, so already-saved scenes load cleanly.
|
||||||
const { nodes: healed } = healSceneNodes(nodes)
|
const { nodes: healed } = healSceneNodes(nodes)
|
||||||
const patchedNodes = { ...healed } as Record<string, any>
|
const patchedNodes = { ...healed } as Record<string, any>
|
||||||
|
|
||||||
// Scene materials minted while moving legacy wall fields onto `node.slots`;
|
// Scene materials minted while moving legacy wall fields onto `node.slots`;
|
||||||
// merged into the scene material map by the caller (`setScene`).
|
// merged into the scene material map by the caller (`setScene`).
|
||||||
const mintedMaterials: Record<SceneMaterialId, SceneMaterial> = {}
|
const mintedMaterials: Record<SceneMaterialId, SceneMaterial> = {}
|
||||||
|
|
||||||
|
// Pass 1: all node types except elevator.
|
||||||
|
// Elevator migration (migrateElevatorParent) mutates level.children to remove
|
||||||
|
// the elevator ID. If the elevator is processed before its parent level in
|
||||||
|
// Object.entries order, the level migration in this same pass would then see
|
||||||
|
// a children array that still contains the elevator ID and filter it out as
|
||||||
|
// "missing" — corrupting the level. Running elevators in a second pass after
|
||||||
|
// all levels are stable avoids the race entirely.
|
||||||
for (const [id, node] of Object.entries(patchedNodes)) {
|
for (const [id, node] of Object.entries(patchedNodes)) {
|
||||||
// 1. Item scale migration
|
// 1. Item scale migration
|
||||||
if (node.type === 'item' && !('scale' in node)) {
|
if (node.type === 'item' && !('scale' in node)) {
|
||||||
@@ -682,14 +691,6 @@ function migrateNodes(nodes: Record<string, any>): {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.type === 'elevator') {
|
|
||||||
const parentMigrated = migrateElevatorParent(id, node, patchedNodes)
|
|
||||||
const normalized = normalizeElevatorNode(parentMigrated)
|
|
||||||
if (normalized) {
|
|
||||||
patchedNodes[id] = normalized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Roof-segment hosting was added in this migration cycle (the same
|
// Roof-segment hosting was added in this migration cycle (the same
|
||||||
// pattern as shelf above). Older segments saved before the schema
|
// pattern as shelf above). Older segments saved before the schema
|
||||||
// gained `children` need the field initialised so
|
// gained `children` need the field initialised so
|
||||||
@@ -778,7 +779,59 @@ function migrateNodes(nodes: Record<string, any>): {
|
|||||||
patchedNodes[id] = { ...node, children: flattened }
|
patchedNodes[id] = { ...node, children: flattened }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Level children normalization.
|
||||||
|
// Pre-0.9.1 JSONs may carry child IDs that no longer exist in the node
|
||||||
|
// map (e.g. elevator IDs that lived under a level before the elevator
|
||||||
|
// parent migration moved them up to building). If those dangling IDs are
|
||||||
|
// left in place, collectReachableNodeIds marks the level as having
|
||||||
|
// reachable children that don't exist, which corrupts the scene graph
|
||||||
|
// traversal and leaves the LevelNode in a broken state — making floors
|
||||||
|
// impossible to drag or delete after import.
|
||||||
|
// We intentionally do NOT filter by type prefix here; being permissive
|
||||||
|
// about which types are allowed as children prevents data loss when new
|
||||||
|
// child types are added to the schema in the future.
|
||||||
|
if (node.type === 'level') {
|
||||||
|
const rawChildren = getStringArray(node.children)
|
||||||
|
const validChildren = rawChildren.filter((childId) => {
|
||||||
|
const exists = Boolean(patchedNodes[childId])
|
||||||
|
if (!exists) {
|
||||||
|
console.warn(
|
||||||
|
'[migrateNodes] level',
|
||||||
|
id,
|
||||||
|
'references missing child',
|
||||||
|
childId,
|
||||||
|
'— dropping',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return exists
|
||||||
|
})
|
||||||
|
const levelNumber = getFiniteNumber(node.level, 0)
|
||||||
|
patchedNodes[id] = {
|
||||||
|
...node,
|
||||||
|
level: levelNumber,
|
||||||
|
children: validChildren,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass 2: elevator migration.
|
||||||
|
// migrateElevatorParent mutates the parent level's children array (removes
|
||||||
|
// the elevator ID from it). Running this after Pass 1 guarantees that the
|
||||||
|
// level normalization above has already seen a clean children list — if we
|
||||||
|
// ran elevator migration inside Pass 1, the order of Object.entries
|
||||||
|
// iteration would be non-deterministic: processing an elevator before its
|
||||||
|
// parent level would mutate the level's children mid-iteration, potentially
|
||||||
|
// causing the level branch above to see a stale node reference.
|
||||||
|
for (const [id, node] of Object.entries(patchedNodes)) {
|
||||||
|
if (node.type !== 'elevator') continue
|
||||||
|
const parentMigrated = migrateElevatorParent(id, node, patchedNodes)
|
||||||
|
const normalized = normalizeElevatorNode(parentMigrated)
|
||||||
|
if (normalized) {
|
||||||
|
patchedNodes[id] = normalized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { nodes: patchedNodes as Record<string, AnyNode>, mintedMaterials }
|
return { nodes: patchedNodes as Record<string, AnyNode>, mintedMaterials }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user