fix(core): preserve legacy site children during scene healing (#544)
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { beforeEach, describe, expect, test } from 'bun:test'
|
||||
import type { AnyNode } from '../schema'
|
||||
import useScene from './use-scene'
|
||||
|
||||
describe('legacy site child migration', () => {
|
||||
beforeEach(() => {
|
||||
useScene.setState({
|
||||
nodes: {},
|
||||
rootNodeIds: [],
|
||||
dirtyNodes: new Set(),
|
||||
collections: {},
|
||||
} as never)
|
||||
useScene.temporal.getState().clear()
|
||||
})
|
||||
|
||||
test('keeps a flat building subtree referenced by an embedded site child', () => {
|
||||
const embeddedBuilding = {
|
||||
object: 'node',
|
||||
id: 'building_legacy',
|
||||
type: 'building',
|
||||
parentId: null,
|
||||
visible: true,
|
||||
metadata: {},
|
||||
children: ['level_legacy'],
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0],
|
||||
}
|
||||
|
||||
useScene.getState().setScene(
|
||||
{
|
||||
site_legacy: {
|
||||
object: 'node',
|
||||
id: 'site_legacy',
|
||||
type: 'site',
|
||||
parentId: null,
|
||||
visible: true,
|
||||
metadata: {},
|
||||
children: [embeddedBuilding],
|
||||
},
|
||||
building_legacy: embeddedBuilding,
|
||||
level_legacy: {
|
||||
object: 'node',
|
||||
id: 'level_legacy',
|
||||
type: 'level',
|
||||
parentId: null,
|
||||
visible: true,
|
||||
metadata: {},
|
||||
children: ['wall_legacy'],
|
||||
level: 0,
|
||||
},
|
||||
wall_legacy: {
|
||||
object: 'node',
|
||||
id: 'wall_legacy',
|
||||
type: 'wall',
|
||||
parentId: 'level_legacy',
|
||||
visible: true,
|
||||
metadata: {},
|
||||
children: [],
|
||||
start: [0, 0],
|
||||
end: [4, 0],
|
||||
},
|
||||
} as unknown as Record<string, AnyNode>,
|
||||
['site_legacy'] as never,
|
||||
)
|
||||
|
||||
const nodes = useScene.getState().nodes
|
||||
expect(Object.keys(nodes).sort()).toEqual([
|
||||
'building_legacy',
|
||||
'level_legacy',
|
||||
'site_legacy',
|
||||
'wall_legacy',
|
||||
])
|
||||
expect((nodes.site_legacy as { children: string[] }).children).toEqual(['building_legacy'])
|
||||
})
|
||||
})
|
||||
@@ -17,6 +17,28 @@ describe('healSceneNodes', () => {
|
||||
expect((nodes.wall_a as { children: string[] }).children).toEqual(['item_x'])
|
||||
})
|
||||
|
||||
test('preserves legacy embedded site children for the scene migration', () => {
|
||||
const building = {
|
||||
id: 'building_legacy',
|
||||
type: 'building',
|
||||
parentId: null,
|
||||
children: ['level_legacy'],
|
||||
}
|
||||
const { nodes, strippedChildRefs } = healSceneNodes({
|
||||
site_legacy: {
|
||||
id: 'site_legacy',
|
||||
type: 'site',
|
||||
parentId: null,
|
||||
children: [building, null],
|
||||
},
|
||||
building_legacy: building,
|
||||
level_legacy: { id: 'level_legacy', type: 'level', parentId: null, children: [] },
|
||||
})
|
||||
|
||||
expect(strippedChildRefs).toBe(1)
|
||||
expect((nodes.site_legacy as { children: unknown[] }).children).toEqual([building])
|
||||
})
|
||||
|
||||
test('drops childless zero-length walls and removes their parent reference', () => {
|
||||
const { nodes, droppedWallIds } = healSceneNodes({
|
||||
level_0: { id: 'level_0', type: 'level', children: ['wall_zero', 'wall_real'] },
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface HealSceneResult {
|
||||
nodes: Record<string, unknown>
|
||||
/** Ids of zero-length walls that were dropped. */
|
||||
droppedWallIds: string[]
|
||||
/** Count of non-string (e.g. null) entries removed from `children` arrays. */
|
||||
/** Count of invalid non-string (e.g. null) entries removed from `children` arrays. */
|
||||
strippedChildRefs: number
|
||||
/**
|
||||
* Count of child references removed because the child's `parentId` points at
|
||||
@@ -74,26 +74,43 @@ export function healSceneNodes(input: Record<string, unknown>): HealSceneResult
|
||||
let strippedChildRefs = 0
|
||||
let strippedStaleChildRefs = 0
|
||||
|
||||
// Pass 2: clean `children` arrays — drop non-string entries (the `[null]`
|
||||
// bug), references to walls we just removed, same-array duplicates, and
|
||||
// stale references whose child's `parentId` names a different parent.
|
||||
// Pass 2: clean `children` arrays — drop invalid non-string entries (the
|
||||
// `[null]` bug), references to walls we just removed, same-array duplicates,
|
||||
// and stale references whose child's `parentId` names a different parent.
|
||||
// Legacy sites embedded full child objects; keep those for migrateNodes to
|
||||
// flatten after healing instead of disconnecting the entire building.
|
||||
const nodes: Record<string, unknown> = {}
|
||||
for (const [id, node] of Object.entries(kept)) {
|
||||
const children = (node as { children?: unknown })?.children
|
||||
if (Array.isArray(children)) {
|
||||
const seen = new Set<string>()
|
||||
const cleaned = children.filter((c): c is string => {
|
||||
if (typeof c !== 'string' || dropped.has(c)) {
|
||||
const cleaned = children.filter((child) => {
|
||||
const embeddedSiteChildId =
|
||||
(node as { type?: unknown }).type === 'site' &&
|
||||
child &&
|
||||
typeof child === 'object' &&
|
||||
typeof (child as { id?: unknown }).id === 'string'
|
||||
? (child as { id: string }).id
|
||||
: null
|
||||
if (embeddedSiteChildId) {
|
||||
if (seen.has(embeddedSiteChildId)) {
|
||||
strippedStaleChildRefs++
|
||||
return false
|
||||
}
|
||||
seen.add(embeddedSiteChildId)
|
||||
return true
|
||||
}
|
||||
if (typeof child !== 'string' || dropped.has(child)) {
|
||||
strippedChildRefs++
|
||||
return false
|
||||
}
|
||||
if (seen.has(c)) {
|
||||
if (seen.has(child)) {
|
||||
strippedStaleChildRefs++
|
||||
return false
|
||||
}
|
||||
seen.add(c)
|
||||
const child = kept[c] as { parentId?: unknown } | undefined
|
||||
if (child && typeof child.parentId === 'string' && child.parentId !== id) {
|
||||
seen.add(child)
|
||||
const childNode = kept[child] as { parentId?: unknown } | undefined
|
||||
if (childNode && typeof childNode.parentId === 'string' && childNode.parentId !== id) {
|
||||
strippedStaleChildRefs++
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user