Skip to content
Open
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
147 changes: 147 additions & 0 deletions packages/core/src/store/use-scene-surface-holes-defaults.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { beforeEach, describe, expect, test } from 'bun:test'
import type { AnyNode } from '../schema'
import useScene from './use-scene'

// Third-party exporters (scanning apps, IFC hand-offs) can legitimately write a
// slab or ceiling without `holes` / `holeMetadata` / `autoFromWalls`: the schema
// declares defaults for them. The opening sync and the public viewer read those
// fields on the raw node, so a missing `holes` threw
// "Cannot read properties of undefined (reading 'map')" and the scene never
// rendered. Doors and windows are already zod-parsed on load; this covers
// slabs and ceilings the same way.
describe('scene load applies surface hole defaults', () => {
beforeEach(() => {
useScene.setState({
nodes: {},
rootNodeIds: [],
dirtyNodes: new Set(),
collections: {},
} as never)
useScene.temporal.getState().clear()
})

const base = {
site_test: {
object: 'node',
id: 'site_test',
type: 'site',
parentId: null,
visible: true,
metadata: {},
children: ['building_test'],
},
building_test: {
object: 'node',
id: 'building_test',
type: 'building',
parentId: 'site_test',
visible: true,
metadata: {},
children: ['level_test'],
},
level_test: {
object: 'node',
id: 'level_test',
type: 'level',
parentId: 'building_test',
visible: true,
metadata: {},
level: 0,
children: ['slab_bare', 'ceiling_bare'],
},
}

test('a slab and a ceiling written without holes load with empty hole arrays', () => {
useScene.getState().setScene(
{
...base,
slab_bare: {
object: 'node',
id: 'slab_bare',
type: 'slab',
parentId: 'level_test',
visible: true,
metadata: {},
polygon: [
[0, 0],
[4, 0],
[4, 3],
[0, 3],
],
elevation: 0.05,
},
ceiling_bare: {
object: 'node',
id: 'ceiling_bare',
type: 'ceiling',
parentId: 'level_test',
visible: true,
metadata: {},
polygon: [
[0, 0],
[4, 0],
[4, 3],
[0, 3],
],
},
} as unknown as Record<AnyNode['id'], AnyNode>,
['site_test' as AnyNode['id']],
)

const slab = useScene.getState().nodes['slab_bare' as AnyNode['id']]
expect(slab?.type).toBe('slab')
if (slab?.type !== 'slab') return
expect(slab.holes).toEqual([])
expect(slab.holeMetadata).toEqual([])
expect(slab.autoFromWalls).toBe(false)
expect(slab.polygon).toEqual([
[0, 0],
[4, 0],
[4, 3],
[0, 3],
])

const ceiling = useScene.getState().nodes['ceiling_bare' as AnyNode['id']]
expect(ceiling?.type).toBe('ceiling')
if (ceiling?.type !== 'ceiling') return
expect(ceiling.holes).toEqual([])
expect(ceiling.holeMetadata).toEqual([])
})

test('existing holes and metadata are kept as written', () => {
useScene.getState().setScene(
{
...base,
level_test: { ...base.level_test, children: ['slab_holed'] },
slab_holed: {
object: 'node',
id: 'slab_holed',
type: 'slab',
parentId: 'level_test',
visible: true,
metadata: {},
polygon: [
[0, 0],
[4, 0],
[4, 3],
[0, 3],
],
holes: [
[
[1, 1],
[2, 1],
[2, 2],
[1, 2],
],
],
holeMetadata: [{ source: 'manual' }],
},
} as unknown as Record<AnyNode['id'], AnyNode>,
['site_test' as AnyNode['id']],
)
const slab = useScene.getState().nodes['slab_holed' as AnyNode['id']]
if (slab?.type !== 'slab') throw new Error('slab expected')
expect(slab.holes).toHaveLength(1)
expect(slab.holeMetadata).toEqual([{ source: 'manual' }])
})
})
20 changes: 20 additions & 0 deletions packages/core/src/store/use-scene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,22 @@ function normalizeWindowNode(node: Record<string, unknown>) {
return parsed.success ? { ...node, ...parsed.data } : null
}

// Slabs and ceilings written by third-party exporters (scanning apps, IFC
// hand-offs) can legitimately omit `holes` / `holeMetadata` / `autoFromWalls`:
// the schema gives them defaults. The opening sync and the public viewer read
// those fields on the raw node, so a missing `holes` throws
// "Cannot read properties of undefined (reading 'map')" and the scene never
// renders. Only the hole fields are filled here — a full zod parse would also
// default `thickness`, which the vertical-model migration below must still see
// as absent on legacy solids.
function normalizeSurfaceHoleFields(node: Record<string, unknown>) {
const holes = Array.isArray(node.holes) ? node.holes : []
const holeMetadata = Array.isArray(node.holeMetadata) ? node.holeMetadata : []
const autoFromWalls =
node.type === 'slab' && node.autoFromWalls === undefined ? { autoFromWalls: false } : {}
return { ...node, holes, holeMetadata, ...autoFromWalls }
}

function normalizeShelfNode(node: Record<string, unknown>) {
const sanitized = {
...node,
Expand Down Expand Up @@ -826,6 +842,10 @@ function migrateNodes(nodes: Record<string, any>): {
}
}

if (node.type === 'slab' || node.type === 'ceiling') {
patchedNodes[id] = normalizeSurfaceHoleFields(patchedNodes[id] ?? node) as AnyNode
}

// Dormers originally rendered one inline parametric window. Promote that
// default to a real hosted WindowNode so additional windows can use the
// regular window tool and inspector without changing the old appearance.
Expand Down
Loading