fix: bug pass — legacy window crash, plant exports, zone snapping/visibility, HDR (#473)
* fix(core): apply window schema defaults on scene load Windows saved before a schema field existed (columnRatios/rowRatios/ frameThickness/…) loaded with those fields missing; the window mesh builder reads them unconditionally and threw every frame, crashing the viewer on legacy scenes. Zod-parse windows on load like doors so the schema defaults land. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): mount plant geometry during client GLB/STL/OBJ export The client export never set isExporting, so instanced kinds (trees/ flowers/grass) kept their colorWrite:false raycast collider mounted and exported it as an opaque white box, while the real geometry (which only mounts while exporting) was never captured. Reuse BakeExporter's flag + frame-wait dance in ExportManager, and harden isRenderableMesh to drop colorWrite:false materials from exports entirely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(editor): route zone drawing through the shared surface snap The zone tool quantized later vertices by distance along a free ray instead of snapping to the grid, and never joined the magnetic wall-corner/midpoint/crossing + alignment-guide pipeline that walls, slabs and ceilings use. Give zone the slab treatment in the 3D tool and both 2D floorplan branches (move + click + double-click commit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(viewer): self-host the scene environment HDR drei's preset="sunset" fetches venice_sunset_1k.hdr from raw.githack.com, which intermittently fails ("Could not load venice_sunset_1k.hdr: Failed to fetch"). Point Environment at /hdri/venice_sunset_1k.hdr and ship the file in the app's public/ — same mirroring convention as /audios/sfx; consuming apps must carry the file too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(editor): unmount zones entirely outside the zones layer Zone labels showed in every editing mode (visual noise), and each zone kept a drei <Html> mounted at opacity 0 — an <Html> costs per-frame matrix work and live DOM even when invisible. A new viewer presentation flag (showZones, default true) lets the editor unmount zone meshes and labels whenever the structure layer isn't 'zones' (and during snapshot capture); the registered group stays so zones keep their scene identity for selection and GLB export. Preview / first-person / viewer surfaces are untouched. Raycast-disable moved from a one-shot group flag to per-frame on the meshes, which now remount on layer toggles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
51fddc2d9c
commit
717c2c5c0a
@@ -0,0 +1,92 @@
|
||||
import { beforeEach, describe, expect, test } from 'bun:test'
|
||||
import type { AnyNode } from '../schema'
|
||||
import useScene from './use-scene'
|
||||
|
||||
describe('scene window migrations', () => {
|
||||
beforeEach(() => {
|
||||
useScene.setState({
|
||||
nodes: {},
|
||||
rootNodeIds: [],
|
||||
dirtyNodes: new Set(),
|
||||
collections: {},
|
||||
} as never)
|
||||
useScene.temporal.getState().clear()
|
||||
})
|
||||
|
||||
test('fills schema defaults on windows saved before a field existed', () => {
|
||||
// Mirrors real legacy scenes (e.g. windows persisted without
|
||||
// columnRatios/rowRatios/frameThickness): the mesh builder reads those
|
||||
// unconditionally, so a missing array crashed the viewer every frame.
|
||||
useScene.getState().setScene(
|
||||
{
|
||||
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: {},
|
||||
children: ['wall_test'],
|
||||
level: 0,
|
||||
},
|
||||
wall_test: {
|
||||
object: 'node',
|
||||
id: 'wall_test',
|
||||
type: 'wall',
|
||||
parentId: 'level_test',
|
||||
visible: true,
|
||||
metadata: {},
|
||||
children: ['window_test'],
|
||||
start: [0, 0],
|
||||
end: [4, 0],
|
||||
height: 2.5,
|
||||
thickness: 0.2,
|
||||
},
|
||||
window_test: {
|
||||
object: 'node',
|
||||
id: 'window_test',
|
||||
type: 'window',
|
||||
parentId: 'wall_test',
|
||||
visible: true,
|
||||
metadata: {},
|
||||
wallId: 'wall_test',
|
||||
position: [1, 1, 0],
|
||||
width: 1.2,
|
||||
height: 1.5,
|
||||
windowType: 'fixed',
|
||||
},
|
||||
} as unknown as Record<string, AnyNode>,
|
||||
['site_test'] as never,
|
||||
)
|
||||
|
||||
const window = useScene.getState().nodes.window_test as Extract<AnyNode, { type: 'window' }>
|
||||
expect(window).toBeDefined()
|
||||
// Schema defaults land on load…
|
||||
expect(window.columnRatios).toEqual([1])
|
||||
expect(window.rowRatios).toEqual([1])
|
||||
expect(window.frameThickness).toBe(0.05)
|
||||
expect(window.sill).toBe(true)
|
||||
// …and authored fields survive.
|
||||
expect(window.width).toBe(1.2)
|
||||
expect(window.height).toBe(1.5)
|
||||
expect(window.wallId).toBe('wall_test')
|
||||
})
|
||||
})
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from '../schema/nodes/stair'
|
||||
import { StairSegmentNode as StairSegmentNodeSchema } from '../schema/nodes/stair-segment'
|
||||
import { getEffectiveWallSurfaceMaterial, type WallSurfaceSide } from '../schema/nodes/wall'
|
||||
import { WindowNode as WindowNodeSchema } from '../schema/nodes/window'
|
||||
import {
|
||||
generateSceneMaterialId,
|
||||
type SceneMaterial,
|
||||
@@ -132,6 +133,14 @@ function normalizeDoorNode(node: Record<string, unknown>) {
|
||||
return parsed.success ? { ...node, ...parsed.data } : null
|
||||
}
|
||||
|
||||
// Windows saved before a schema field existed (e.g. `columnRatios`/`rowRatios`/
|
||||
// `frameThickness`) load without it; the mesh builder then reads undefined and
|
||||
// throws every frame. Zod-parse on load so schema defaults land, like doors.
|
||||
function normalizeWindowNode(node: Record<string, unknown>) {
|
||||
const parsed = WindowNodeSchema.safeParse(node)
|
||||
return parsed.success ? { ...node, ...parsed.data } : null
|
||||
}
|
||||
|
||||
function normalizeShelfNode(node: Record<string, unknown>) {
|
||||
const sanitized = {
|
||||
...node,
|
||||
@@ -640,6 +649,13 @@ function migrateNodes(nodes: Record<string, any>): {
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === 'window') {
|
||||
const normalized = normalizeWindowNode(node)
|
||||
if (normalized) {
|
||||
patchedNodes[id] = normalized
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === 'stair') {
|
||||
const normalized = normalizeStairNode(migrateStairSurfaceMaterials(node))
|
||||
if (normalized) {
|
||||
|
||||
Reference in New Issue
Block a user