Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions packages/core/src/store/use-scene-site-child-migration.test.ts
Original file line number Diff line number Diff line change
@@ -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'])
})
})
22 changes: 22 additions & 0 deletions packages/core/src/utils/heal-scene-graph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'] },
Expand Down
37 changes: 27 additions & 10 deletions packages/core/src/utils/heal-scene-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
Loading