fix(viewer): normalize legacy door nodes against the schema before render (#303)
Legacy/unparsed door nodes can miss schema-defaulted fields (segments, columnRatios, dividerThickness, panelInset/panelDepth …) and crash the door geometry build (EDITOR-AM). `updateDoorMesh` re-applies the Zod defaults once at entry via `DoorNodeSchema.safeParse` (with a drop-bad-segments retry and a full-defaults fallback), so every downstream read sees populated data — including the shaped-top divider path #334 added. Also normalizes schema-valid legacy doors at load in `migrateNodes`, mirroring the existing stair normalizer, so the door panel and window system see defaulted data too. Reimplemented against main: the original used `segments ?? []` (wrong — legacy doors rendered an empty frame) and missed the #334 `dividerThickness` sites. Verified: `bun run check-types` clean, biome clean on touched files.
This commit is contained in:
@@ -6,6 +6,7 @@ import { create, type StoreApi, type UseBoundStore } from 'zustand'
|
|||||||
import { BuildingNode } from '../schema'
|
import { BuildingNode } from '../schema'
|
||||||
import type { Collection, CollectionId } from '../schema/collections'
|
import type { Collection, CollectionId } from '../schema/collections'
|
||||||
import { generateCollectionId } from '../schema/collections'
|
import { generateCollectionId } from '../schema/collections'
|
||||||
|
import { DoorNode as DoorNodeSchema } from '../schema/nodes/door'
|
||||||
import { LevelNode } from '../schema/nodes/level'
|
import { LevelNode } from '../schema/nodes/level'
|
||||||
import {
|
import {
|
||||||
getPitchFromActiveRoofHeight,
|
getPitchFromActiveRoofHeight,
|
||||||
@@ -106,6 +107,11 @@ function normalizeStairSegmentNode(node: Record<string, unknown>) {
|
|||||||
return parsed.success ? parsed.data : null
|
return parsed.success ? parsed.data : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeDoorNode(node: Record<string, unknown>) {
|
||||||
|
const parsed = DoorNodeSchema.safeParse(node)
|
||||||
|
return parsed.success ? { ...node, ...parsed.data } : null
|
||||||
|
}
|
||||||
|
|
||||||
function migrateWallSurfaceMaterials(node: Record<string, any>) {
|
function migrateWallSurfaceMaterials(node: Record<string, any>) {
|
||||||
const hasInterior =
|
const hasInterior =
|
||||||
node.interiorMaterial !== undefined || typeof node.interiorMaterialPreset === 'string'
|
node.interiorMaterial !== undefined || typeof node.interiorMaterialPreset === 'string'
|
||||||
@@ -365,6 +371,13 @@ function migrateNodes(nodes: Record<string, any>): Record<string, AnyNode> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node.type === 'door') {
|
||||||
|
const normalized = normalizeDoorNode(node)
|
||||||
|
if (normalized) {
|
||||||
|
patchedNodes[id] = normalized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (node.type === 'stair') {
|
if (node.type === 'stair') {
|
||||||
const normalized = normalizeStairNode(migrateStairSurfaceMaterials(node))
|
const normalized = normalizeStairNode(migrateStairSurfaceMaterials(node))
|
||||||
if (normalized) {
|
if (normalized) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
type AnyNodeId,
|
type AnyNodeId,
|
||||||
clampDoorOperationState,
|
clampDoorOperationState,
|
||||||
type DoorNode,
|
type DoorNode,
|
||||||
|
DoorNode as DoorNodeSchema,
|
||||||
getDoorRenderOpenAmount,
|
getDoorRenderOpenAmount,
|
||||||
getEffectiveNode,
|
getEffectiveNode,
|
||||||
sceneRegistry,
|
sceneRegistry,
|
||||||
@@ -26,6 +27,20 @@ let baseMaterial = getBaseMaterial()
|
|||||||
let revealMaterial: THREE.Material = defaultRevealMaterial
|
let revealMaterial: THREE.Material = defaultRevealMaterial
|
||||||
let glassMaterial: THREE.Material = defaultGlassMaterial
|
let glassMaterial: THREE.Material = defaultGlassMaterial
|
||||||
|
|
||||||
|
const DOOR_RENDER_DEFAULTS = DoorNodeSchema.parse({ id: 'door_render_default' })
|
||||||
|
|
||||||
|
// Legacy/unparsed door nodes can miss schema-defaulted fields (segments,
|
||||||
|
// columnRatios, dividerThickness, …) and crash the geometry build. Re-apply the
|
||||||
|
// Zod defaults; if the node is structurally invalid (e.g. a segment missing a
|
||||||
|
// required field) drop the bad segments, then fall back to defaults entirely.
|
||||||
|
function normalizeDoorNodeForRender(node: DoorNode): DoorNode {
|
||||||
|
const parsed = DoorNodeSchema.safeParse(node)
|
||||||
|
if (parsed.success) return parsed.data
|
||||||
|
const retry = DoorNodeSchema.safeParse({ ...node, segments: undefined })
|
||||||
|
if (retry.success) return retry.data
|
||||||
|
return { ...DOOR_RENDER_DEFAULTS, id: node.id, parentId: node.parentId }
|
||||||
|
}
|
||||||
|
|
||||||
export const DoorSystem = () => {
|
export const DoorSystem = () => {
|
||||||
const dirtyNodes = useScene((state) => state.dirtyNodes)
|
const dirtyNodes = useScene((state) => state.dirtyNodes)
|
||||||
const clearDirty = useScene((state) => state.clearDirty)
|
const clearDirty = useScene((state) => state.clearDirty)
|
||||||
@@ -1903,7 +1918,9 @@ function getEffectiveOpeningShape(node: DoorNode): DoorNode['openingShape'] {
|
|||||||
: (node.openingShape ?? 'rectangle')
|
: (node.openingShape ?? 'rectangle')
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDoorMesh(node: DoorNode, mesh: THREE.Mesh) {
|
function updateDoorMesh(rawNode: DoorNode, mesh: THREE.Mesh) {
|
||||||
|
const node = normalizeDoorNodeForRender(rawNode)
|
||||||
|
|
||||||
// Root mesh is an invisible hitbox; all visuals live in child meshes
|
// Root mesh is an invisible hitbox; all visuals live in child meshes
|
||||||
mesh.geometry.dispose()
|
mesh.geometry.dispose()
|
||||||
mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth)
|
mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth)
|
||||||
|
|||||||
Reference in New Issue
Block a user