@pascal-app/core 0.9.1 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -10
- package/dist/events/bus.d.ts +49 -2
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/hooks/scene-registry/item-clip-registry.d.ts +18 -0
- package/dist/hooks/scene-registry/item-clip-registry.d.ts.map +1 -0
- package/dist/hooks/scene-registry/item-clip-registry.js +10 -0
- package/dist/hooks/scene-registry/scene-registry.d.ts +8 -1
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.js +27 -1
- package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +17 -1
- package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/floor-placed-elevation.js +61 -8
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +135 -28
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +427 -271
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +46 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +208 -9
- package/dist/hooks/spatial-grid/support-host-id.d.ts +3 -0
- package/dist/hooks/spatial-grid/support-host-id.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/support-host-id.js +2 -0
- package/dist/hooks/spatial-grid/support-host-patch.d.ts +51 -0
- package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/support-host-patch.js +164 -0
- package/dist/index.d.ts +33 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -10
- package/dist/lib/level-name.d.ts +4 -0
- package/dist/lib/level-name.d.ts.map +1 -0
- package/dist/lib/level-name.js +10 -0
- package/dist/lib/measurement-geometry.d.ts +23 -0
- package/dist/lib/measurement-geometry.d.ts.map +1 -0
- package/dist/lib/measurement-geometry.js +249 -0
- package/dist/lib/polygon-geometry.d.ts +0 -1
- package/dist/lib/polygon-geometry.d.ts.map +1 -1
- package/dist/lib/polygon-geometry.js +0 -18
- package/dist/lib/selection-proxy.d.ts +4 -0
- package/dist/lib/selection-proxy.d.ts.map +1 -0
- package/dist/lib/selection-proxy.js +12 -0
- package/dist/lib/slab-polygon.d.ts +31 -2
- package/dist/lib/slab-polygon.d.ts.map +1 -1
- package/dist/lib/slab-polygon.js +557 -31
- package/dist/lib/slots.d.ts +14 -0
- package/dist/lib/slots.d.ts.map +1 -0
- package/dist/lib/slots.js +26 -0
- package/dist/lib/space-detection.d.ts +40 -6
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +574 -192
- package/dist/lib/terrain-brush.d.ts +163 -0
- package/dist/lib/terrain-brush.d.ts.map +1 -0
- package/dist/lib/terrain-brush.js +359 -0
- package/dist/lib/terrain-codec.d.ts +51 -0
- package/dist/lib/terrain-codec.d.ts.map +1 -0
- package/dist/lib/terrain-codec.js +167 -0
- package/dist/lib/terrain-field.d.ts +150 -0
- package/dist/lib/terrain-field.d.ts.map +1 -0
- package/dist/lib/terrain-field.js +248 -0
- package/dist/lib/terrain-raycast.d.ts +44 -0
- package/dist/lib/terrain-raycast.d.ts.map +1 -0
- package/dist/lib/terrain-raycast.js +188 -0
- package/dist/lib/terrain-source.d.ts +60 -0
- package/dist/lib/terrain-source.d.ts.map +1 -0
- package/dist/lib/terrain-source.js +90 -0
- package/dist/lib/terrain-support.d.ts +60 -0
- package/dist/lib/terrain-support.d.ts.map +1 -0
- package/dist/lib/terrain-support.js +150 -0
- package/dist/lib/wall-distance.d.ts +60 -0
- package/dist/lib/wall-distance.d.ts.map +1 -0
- package/dist/lib/wall-distance.js +85 -0
- package/dist/lib/zone-quantities.d.ts +21 -0
- package/dist/lib/zone-quantities.d.ts.map +1 -0
- package/dist/lib/zone-quantities.js +428 -0
- package/dist/material-library.d.ts +28 -1
- package/dist/material-library.d.ts.map +1 -1
- package/dist/material-library.js +2233 -441
- package/dist/registry/handles.d.ts +91 -3
- package/dist/registry/handles.d.ts.map +1 -1
- package/dist/registry/index.d.ts +3 -3
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +1 -1
- package/dist/registry/registry.d.ts +46 -3
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/registry.js +82 -1
- package/dist/registry/subtree.d.ts.map +1 -1
- package/dist/registry/subtree.js +8 -1
- package/dist/registry/types.d.ts +781 -39
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/schema/index.d.ts +27 -8
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +23 -6
- package/dist/schema/material.d.ts +3 -2
- package/dist/schema/material.d.ts.map +1 -1
- package/dist/schema/material.js +7 -5
- package/dist/schema/nodes/box-vent.d.ts +2 -2
- package/dist/schema/nodes/cabinet.d.ts +536 -0
- package/dist/schema/nodes/cabinet.d.ts.map +1 -0
- package/dist/schema/nodes/cabinet.js +139 -0
- package/dist/schema/nodes/ceiling.d.ts +4 -3
- package/dist/schema/nodes/ceiling.d.ts.map +1 -1
- package/dist/schema/nodes/ceiling.js +11 -1
- package/dist/schema/nodes/chimney.d.ts +4 -4
- package/dist/schema/nodes/column.d.ts +4 -2
- package/dist/schema/nodes/column.d.ts.map +1 -1
- package/dist/schema/nodes/column.js +5 -0
- package/dist/schema/nodes/construction-dimension.d.ts +195 -0
- package/dist/schema/nodes/construction-dimension.d.ts.map +1 -0
- package/dist/schema/nodes/construction-dimension.js +151 -0
- package/dist/schema/nodes/cupola.d.ts +2 -2
- package/dist/schema/nodes/door.d.ts +42 -5
- package/dist/schema/nodes/door.d.ts.map +1 -1
- package/dist/schema/nodes/door.js +32 -0
- package/dist/schema/nodes/dormer.d.ts +8 -8
- package/dist/schema/nodes/dormer.js +1 -1
- package/dist/schema/nodes/downspout.d.ts +2 -2
- package/dist/schema/nodes/duct-fitting.d.ts +91 -0
- package/dist/schema/nodes/duct-fitting.d.ts.map +1 -0
- package/dist/schema/nodes/duct-fitting.js +92 -0
- package/dist/schema/nodes/duct-segment.d.ts +61 -0
- package/dist/schema/nodes/duct-segment.d.ts.map +1 -0
- package/dist/schema/nodes/duct-segment.js +73 -0
- package/dist/schema/nodes/duct-terminal.d.ts +58 -0
- package/dist/schema/nodes/duct-terminal.d.ts.map +1 -0
- package/dist/schema/nodes/duct-terminal.js +53 -0
- package/dist/schema/nodes/elevator.d.ts +3 -2
- package/dist/schema/nodes/elevator.d.ts.map +1 -1
- package/dist/schema/nodes/elevator.js +3 -0
- package/dist/schema/nodes/eyebrow-vent.d.ts +2 -2
- package/dist/schema/nodes/fence.d.ts +20 -2
- package/dist/schema/nodes/fence.d.ts.map +1 -1
- package/dist/schema/nodes/fence.js +33 -2
- package/dist/schema/nodes/gutter.d.ts +2 -2
- package/dist/schema/nodes/hvac-equipment.d.ts +64 -0
- package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -0
- package/dist/schema/nodes/hvac-equipment.js +57 -0
- package/dist/schema/nodes/item.d.ts +9 -0
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +24 -0
- package/dist/schema/nodes/level.d.ts +2 -1
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +12 -31
- package/dist/schema/nodes/lineset.d.ts +42 -0
- package/dist/schema/nodes/lineset.d.ts.map +1 -0
- package/dist/schema/nodes/lineset.js +38 -0
- package/dist/schema/nodes/liquid-line.d.ts +34 -0
- package/dist/schema/nodes/liquid-line.d.ts.map +1 -0
- package/dist/schema/nodes/liquid-line.js +24 -0
- package/dist/schema/nodes/measurement.d.ts +306 -0
- package/dist/schema/nodes/measurement.d.ts.map +1 -0
- package/dist/schema/nodes/measurement.js +87 -0
- package/dist/schema/nodes/pipe-fitting.d.ts +56 -0
- package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -0
- package/dist/schema/nodes/pipe-fitting.js +45 -0
- package/dist/schema/nodes/pipe-segment.d.ts +48 -0
- package/dist/schema/nodes/pipe-segment.d.ts.map +1 -0
- package/dist/schema/nodes/pipe-segment.js +36 -0
- package/dist/schema/nodes/pipe-trap.d.ts +43 -0
- package/dist/schema/nodes/pipe-trap.d.ts.map +1 -0
- package/dist/schema/nodes/pipe-trap.js +36 -0
- package/dist/schema/nodes/ridge-vent.d.ts +18 -3
- package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
- package/dist/schema/nodes/ridge-vent.js +221 -3
- package/dist/schema/nodes/roof-segment-shape.d.ts +90 -0
- package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-shape.js +318 -0
- package/dist/schema/nodes/roof-segment-walls.d.ts +101 -0
- package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-walls.js +330 -0
- package/dist/schema/nodes/roof-segment.d.ts +84 -8
- package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment.js +290 -5
- package/dist/schema/nodes/roof.d.ts +8 -8
- package/dist/schema/nodes/shelf.d.ts +4 -2
- package/dist/schema/nodes/shelf.d.ts.map +1 -1
- package/dist/schema/nodes/shelf.js +8 -0
- package/dist/schema/nodes/site.d.ts +9 -0
- package/dist/schema/nodes/site.d.ts.map +1 -1
- package/dist/schema/nodes/site.js +8 -6
- package/dist/schema/nodes/skylight.d.ts +10 -10
- package/dist/schema/nodes/slab.d.ts +8 -2
- package/dist/schema/nodes/slab.d.ts.map +1 -1
- package/dist/schema/nodes/slab.js +18 -2
- package/dist/schema/nodes/solar-panel.d.ts +4 -4
- package/dist/schema/nodes/spawn.d.ts +1 -0
- package/dist/schema/nodes/spawn.d.ts.map +1 -1
- package/dist/schema/nodes/spawn.js +2 -0
- package/dist/schema/nodes/stair-segment.d.ts +4 -4
- package/dist/schema/nodes/stair.d.ts +16 -13
- package/dist/schema/nodes/stair.d.ts.map +1 -1
- package/dist/schema/nodes/stair.js +11 -1
- package/dist/schema/nodes/structural-grid.d.ts +27 -0
- package/dist/schema/nodes/structural-grid.d.ts.map +1 -0
- package/dist/schema/nodes/structural-grid.js +17 -0
- package/dist/schema/nodes/turbine-vent.d.ts +2 -2
- package/dist/schema/nodes/wall.d.ts +248 -6
- package/dist/schema/nodes/wall.d.ts.map +1 -1
- package/dist/schema/nodes/wall.js +266 -0
- package/dist/schema/nodes/window.d.ts +40 -3
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/nodes/window.js +30 -0
- package/dist/schema/nodes/zone.d.ts +22 -0
- package/dist/schema/nodes/zone.d.ts.map +1 -1
- package/dist/schema/nodes/zone.js +21 -0
- package/dist/schema/scene-material.d.ts +41 -0
- package/dist/schema/scene-material.d.ts.map +1 -0
- package/dist/schema/scene-material.js +9 -0
- package/dist/schema/terrain.d.ts +28 -0
- package/dist/schema/terrain.d.ts.map +1 -0
- package/dist/schema/terrain.js +30 -0
- package/dist/schema/types.d.ts +1300 -97
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +27 -0
- package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts +80 -0
- package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts.map +1 -0
- package/dist/services/__fixtures__/wall-plane-top-boundary-repro.js +89 -0
- package/dist/services/alignment-anchors.d.ts.map +1 -1
- package/dist/services/alignment-anchors.js +30 -1
- package/dist/services/alignment.d.ts +5 -0
- package/dist/services/alignment.d.ts.map +1 -1
- package/dist/services/alignment.js +2 -0
- package/dist/services/hosting.d.ts +8 -0
- package/dist/services/hosting.d.ts.map +1 -1
- package/dist/services/hosting.js +13 -4
- package/dist/services/index.d.ts +9 -2
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +9 -2
- package/dist/services/level-height.d.ts +27 -0
- package/dist/services/level-height.d.ts.map +1 -0
- package/dist/services/level-height.js +94 -0
- package/dist/services/opening-guides.d.ts +141 -0
- package/dist/services/opening-guides.d.ts.map +1 -0
- package/dist/services/opening-guides.js +267 -0
- package/dist/services/port-connectivity.d.ts +115 -0
- package/dist/services/port-connectivity.d.ts.map +1 -0
- package/dist/services/port-connectivity.js +300 -0
- package/dist/services/riser-diagram.d.ts +40 -0
- package/dist/services/riser-diagram.d.ts.map +1 -0
- package/dist/services/riser-diagram.js +110 -0
- package/dist/services/snap.d.ts +9 -1
- package/dist/services/snap.d.ts.map +1 -1
- package/dist/services/snap.js +21 -1
- package/dist/services/storey.d.ts +103 -0
- package/dist/services/storey.d.ts.map +1 -0
- package/dist/services/storey.js +296 -0
- package/dist/services/system-graph.d.ts +30 -0
- package/dist/services/system-graph.d.ts.map +1 -0
- package/dist/services/system-graph.js +164 -0
- package/dist/services/validate-dwv.d.ts +32 -0
- package/dist/services/validate-dwv.d.ts.map +1 -0
- package/dist/services/validate-dwv.js +118 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +141 -4
- package/dist/store/history-control.d.ts +31 -0
- package/dist/store/history-control.d.ts.map +1 -1
- package/dist/store/history-control.js +130 -0
- package/dist/store/use-live-terrain.d.ts +65 -0
- package/dist/store/use-live-terrain.d.ts.map +1 -0
- package/dist/store/use-live-terrain.js +31 -0
- package/dist/store/use-live-transforms.d.ts +8 -0
- package/dist/store/use-live-transforms.d.ts.map +1 -1
- package/dist/store/use-scene.d.ts +47 -3
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +992 -55
- package/dist/systems/elevator/elevator-opening-system.d.ts +1 -0
- package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-opening-system.js +27 -21
- package/dist/systems/elevator/elevator-service.d.ts +0 -2
- package/dist/systems/elevator/elevator-service.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-service.js +2 -24
- package/dist/systems/fence/fence-centerline.d.ts +12 -0
- package/dist/systems/fence/fence-centerline.d.ts.map +1 -0
- package/dist/systems/fence/fence-centerline.js +37 -0
- package/dist/systems/fence/fence-spline.d.ts +39 -0
- package/dist/systems/fence/fence-spline.d.ts.map +1 -0
- package/dist/systems/fence/fence-spline.js +196 -0
- package/dist/systems/slab/slab-placement.d.ts +11 -0
- package/dist/systems/slab/slab-placement.d.ts.map +1 -0
- package/dist/systems/slab/slab-placement.js +14 -0
- package/dist/systems/slab/slab-support.d.ts +137 -0
- package/dist/systems/slab/slab-support.d.ts.map +1 -0
- package/dist/systems/slab/slab-support.js +557 -0
- package/dist/systems/stair/stair-footprint.d.ts.map +1 -1
- package/dist/systems/stair/stair-footprint.js +21 -11
- package/dist/systems/stair/stair-opening-preview.d.ts +1300 -505
- package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.js +19 -33
- package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-system.js +32 -4
- package/dist/systems/stair/stair-rise.d.ts +22 -0
- package/dist/systems/stair/stair-rise.d.ts.map +1 -0
- package/dist/systems/stair/stair-rise.js +79 -0
- package/dist/systems/wall/wall-curve.d.ts +10 -0
- package/dist/systems/wall/wall-curve.d.ts.map +1 -1
- package/dist/systems/wall/wall-curve.js +1 -1
- package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
- package/dist/systems/wall/wall-mitering.js +44 -8
- package/dist/systems/wall/wall-top.d.ts +40 -0
- package/dist/systems/wall/wall-top.d.ts.map +1 -0
- package/dist/systems/wall/wall-top.js +46 -0
- package/dist/utils/clone-scene-graph.d.ts +1 -0
- package/dist/utils/clone-scene-graph.d.ts.map +1 -1
- package/dist/utils/clone-scene-graph.js +56 -5
- package/dist/utils/heal-scene-graph.d.ts +18 -0
- package/dist/utils/heal-scene-graph.d.ts.map +1 -0
- package/dist/utils/heal-scene-graph.js +106 -0
- package/dist/validation/validate-build-json.d.ts +3 -0
- package/dist/validation/validate-build-json.d.ts.map +1 -1
- package/dist/validation/validate-build-json.js +90 -5
- package/package.json +4 -4
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +0 -2
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +0 -1
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +0 -273
- package/dist/lib/space-detection-pause.test.d.ts +0 -2
- package/dist/lib/space-detection-pause.test.d.ts.map +0 -1
- package/dist/lib/space-detection-pause.test.js +0 -42
- package/dist/lib/space-detection.test.d.ts +0 -2
- package/dist/lib/space-detection.test.d.ts.map +0 -1
- package/dist/lib/space-detection.test.js +0 -77
- package/dist/registry/registry.test.d.ts +0 -2
- package/dist/registry/registry.test.d.ts.map +0 -1
- package/dist/registry/registry.test.js +0 -183
- package/dist/registry/relations-resolver.test.d.ts +0 -2
- package/dist/registry/relations-resolver.test.d.ts.map +0 -1
- package/dist/registry/relations-resolver.test.js +0 -183
- package/dist/registry/scene-api.test.d.ts +0 -2
- package/dist/registry/scene-api.test.d.ts.map +0 -1
- package/dist/registry/scene-api.test.js +0 -183
- package/dist/registry/subtree.test.d.ts +0 -2
- package/dist/registry/subtree.test.d.ts.map +0 -1
- package/dist/registry/subtree.test.js +0 -133
- package/dist/schema/asset-url.test.d.ts +0 -2
- package/dist/schema/asset-url.test.d.ts.map +0 -1
- package/dist/schema/asset-url.test.js +0 -134
- package/dist/services/alignment-anchors.test.d.ts +0 -2
- package/dist/services/alignment-anchors.test.d.ts.map +0 -1
- package/dist/services/alignment-anchors.test.js +0 -383
- package/dist/services/alignment.test.d.ts +0 -2
- package/dist/services/alignment.test.d.ts.map +0 -1
- package/dist/services/alignment.test.js +0 -92
- package/dist/services/drag-session.test.d.ts +0 -2
- package/dist/services/drag-session.test.d.ts.map +0 -1
- package/dist/services/drag-session.test.js +0 -219
- package/dist/services/hosting.test.d.ts +0 -2
- package/dist/services/hosting.test.d.ts.map +0 -1
- package/dist/services/hosting.test.js +0 -199
- package/dist/services/movement.test.d.ts +0 -2
- package/dist/services/movement.test.d.ts.map +0 -1
- package/dist/services/movement.test.js +0 -133
- package/dist/services/single-undo-dance.test.d.ts +0 -2
- package/dist/services/single-undo-dance.test.d.ts.map +0 -1
- package/dist/services/single-undo-dance.test.js +0 -210
- package/dist/services/snap.test.d.ts +0 -2
- package/dist/services/snap.test.d.ts.map +0 -1
- package/dist/services/snap.test.js +0 -95
- package/dist/store/actions/node-mutation-sanitize.test.d.ts +0 -2
- package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +0 -1
- package/dist/store/actions/node-mutation-sanitize.test.js +0 -147
- package/dist/store/actions/reparent.test.d.ts +0 -2
- package/dist/store/actions/reparent.test.d.ts.map +0 -1
- package/dist/store/actions/reparent.test.js +0 -212
- package/dist/store/use-scene-elevator-migration.test.d.ts +0 -2
- package/dist/store/use-scene-elevator-migration.test.d.ts.map +0 -1
- package/dist/store/use-scene-elevator-migration.test.js +0 -109
- package/dist/systems/elevator/elevator-opening-sync.test.d.ts +0 -2
- package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +0 -1
- package/dist/systems/elevator/elevator-opening-sync.test.js +0 -146
- package/dist/systems/elevator/elevator-runtime.test.d.ts +0 -2
- package/dist/systems/elevator/elevator-runtime.test.d.ts.map +0 -1
- package/dist/systems/elevator/elevator-runtime.test.js +0 -64
- package/dist/systems/stair/stair-opening-preview.test.d.ts +0 -2
- package/dist/systems/stair/stair-opening-preview.test.d.ts.map +0 -1
- package/dist/systems/stair/stair-opening-preview.test.js +0 -80
- package/dist/systems/stair/stair-opening-sync.test.d.ts +0 -2
- package/dist/systems/stair/stair-opening-sync.test.d.ts.map +0 -1
- package/dist/systems/stair/stair-opening-sync.test.js +0 -446
- package/dist/utils/clone-scene-graph.test.d.ts +0 -2
- package/dist/utils/clone-scene-graph.test.d.ts.map +0 -1
- package/dist/utils/clone-scene-graph.test.js +0 -59
package/dist/store/use-scene.js
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { temporal } from 'zundo';
|
|
3
3
|
import { create } from 'zustand';
|
|
4
|
+
import { parseMaterialRef, toSceneMaterialRef } from '../material-library';
|
|
5
|
+
import { getNodePluginId, isNodeKindEnabled, nodeRegistry } from '../registry/registry';
|
|
4
6
|
import { BuildingNode } from '../schema';
|
|
5
7
|
import { generateCollectionId } from '../schema/collections';
|
|
6
8
|
import { DoorNode as DoorNodeSchema } from '../schema/nodes/door';
|
|
7
9
|
import { ElevatorNode as ElevatorNodeSchema } from '../schema/nodes/elevator';
|
|
8
10
|
import { LevelNode } from '../schema/nodes/level';
|
|
9
11
|
import { getPitchFromActiveRoofHeight, } from '../schema/nodes/roof-segment';
|
|
12
|
+
import { segmentPointToRoofWallFace } from '../schema/nodes/roof-segment-walls';
|
|
10
13
|
import { ShelfNode as ShelfNodeSchema } from '../schema/nodes/shelf';
|
|
11
14
|
import { SiteNode } from '../schema/nodes/site';
|
|
12
|
-
import { StairNode as StairNodeSchema } from '../schema/nodes/stair';
|
|
15
|
+
import { getEffectiveStairSurfaceMaterial, StairNode as StairNodeSchema, } from '../schema/nodes/stair';
|
|
13
16
|
import { StairSegmentNode as StairSegmentNodeSchema } from '../schema/nodes/stair-segment';
|
|
17
|
+
import { getEffectiveWallSurfaceMaterial } from '../schema/nodes/wall';
|
|
18
|
+
import { WindowNode as WindowNodeSchema } from '../schema/nodes/window';
|
|
19
|
+
import { generateSceneMaterialId, SceneMaterial, } from '../schema/scene-material';
|
|
20
|
+
import { AnyNode as AnyNodeSchema } from '../schema/types';
|
|
21
|
+
import { deriveLegacyLevelHeight } from '../services/level-height';
|
|
22
|
+
import { getCeilingClampBound } from '../services/storey';
|
|
23
|
+
import { computeWallSlabSupport } from '../systems/slab/slab-support';
|
|
24
|
+
import { DEFAULT_WALL_HEIGHT } from '../systems/wall/wall-footprint';
|
|
25
|
+
import { healSceneNodes } from '../utils/heal-scene-graph';
|
|
14
26
|
import * as nodeActions from './actions/node-actions';
|
|
15
|
-
import { resetSceneHistoryPauseDepth } from './history-control';
|
|
27
|
+
import { areSceneSnapshotsEqual, getSceneHistoryPauseDepth, notifySceneCommit, pauseSceneHistory, resetSceneHistoryPauseDepth, resumeSceneHistory, } from './history-control';
|
|
28
|
+
import useLiveNodeOverrides from './use-live-node-overrides';
|
|
29
|
+
import useLiveTransforms from './use-live-transforms';
|
|
16
30
|
function getFiniteNumber(value, fallback) {
|
|
17
31
|
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
18
32
|
}
|
|
@@ -45,6 +59,7 @@ function getVector3(value, fallback) {
|
|
|
45
59
|
];
|
|
46
60
|
}
|
|
47
61
|
function normalizeStairNode(node) {
|
|
62
|
+
const hasTotalRise = 'totalRise' in node;
|
|
48
63
|
const sanitized = {
|
|
49
64
|
...node,
|
|
50
65
|
position: getVector3(node.position, [0, 0, 0]),
|
|
@@ -55,7 +70,7 @@ function normalizeStairNode(node) {
|
|
|
55
70
|
slabOpeningMode: getEnumValue(node.slabOpeningMode, ['none', 'destination'], 'none'),
|
|
56
71
|
openingOffset: getFiniteNumber(node.openingOffset, 0),
|
|
57
72
|
width: getFiniteNumber(node.width, 1),
|
|
58
|
-
totalRise: getFiniteNumber(node.totalRise, 2.5),
|
|
73
|
+
totalRise: hasTotalRise ? getFiniteNumber(node.totalRise, 2.5) : undefined,
|
|
59
74
|
stepCount: getFiniteNumber(node.stepCount, 10),
|
|
60
75
|
thickness: getFiniteNumber(node.thickness, 0.25),
|
|
61
76
|
fillToFloor: getBoolean(node.fillToFloor, true),
|
|
@@ -70,7 +85,15 @@ function normalizeStairNode(node) {
|
|
|
70
85
|
children: getStringArray(node.children),
|
|
71
86
|
};
|
|
72
87
|
const parsed = StairNodeSchema.safeParse(sanitized);
|
|
73
|
-
|
|
88
|
+
if (!parsed.success)
|
|
89
|
+
return null;
|
|
90
|
+
if (hasTotalRise)
|
|
91
|
+
return parsed.data;
|
|
92
|
+
// Absent `totalRise` means "rise derives from the storey height" and must
|
|
93
|
+
// survive the load: safeParse echoes the sanitized explicit-undefined key,
|
|
94
|
+
// which would flip `'totalRise' in node` checks — strip it back off.
|
|
95
|
+
const { totalRise: _totalRise, ...rest } = parsed.data;
|
|
96
|
+
return rest;
|
|
74
97
|
}
|
|
75
98
|
function normalizeStairSegmentNode(node) {
|
|
76
99
|
const sanitized = {
|
|
@@ -93,6 +116,13 @@ function normalizeDoorNode(node) {
|
|
|
93
116
|
const parsed = DoorNodeSchema.safeParse(node);
|
|
94
117
|
return parsed.success ? { ...node, ...parsed.data } : null;
|
|
95
118
|
}
|
|
119
|
+
// Windows saved before a schema field existed (e.g. `columnRatios`/`rowRatios`/
|
|
120
|
+
// `frameThickness`) load without it; the mesh builder then reads undefined and
|
|
121
|
+
// throws every frame. Zod-parse on load so schema defaults land, like doors.
|
|
122
|
+
function normalizeWindowNode(node) {
|
|
123
|
+
const parsed = WindowNodeSchema.safeParse(node);
|
|
124
|
+
return parsed.success ? { ...node, ...parsed.data } : null;
|
|
125
|
+
}
|
|
96
126
|
function normalizeShelfNode(node) {
|
|
97
127
|
const sanitized = {
|
|
98
128
|
...node,
|
|
@@ -182,40 +212,127 @@ function migrateElevatorParent(id, node, nodes) {
|
|
|
182
212
|
parentId: buildingId,
|
|
183
213
|
};
|
|
184
214
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) {
|
|
194
|
-
return node;
|
|
215
|
+
// Reuse an already-minted scene material for an identical inline legacy
|
|
216
|
+
// material so a whole building painted one custom colour collapses to one
|
|
217
|
+
// shared datablock (mirrors `commitSlotPaint`'s dedupe-on-match).
|
|
218
|
+
function findMintedSceneMaterialRef(material, mintedMaterials) {
|
|
219
|
+
const target = JSON.stringify(material);
|
|
220
|
+
for (const sceneMaterial of Object.values(mintedMaterials)) {
|
|
221
|
+
if (JSON.stringify(sceneMaterial.material) === target) {
|
|
222
|
+
return toSceneMaterialRef(sceneMaterial.id);
|
|
195
223
|
}
|
|
196
|
-
return {
|
|
197
|
-
...node,
|
|
198
|
-
interiorMaterial: legacyFinish.material,
|
|
199
|
-
interiorMaterialPreset: legacyFinish.materialPreset,
|
|
200
|
-
exteriorMaterial: legacyFinish.material,
|
|
201
|
-
exteriorMaterialPreset: legacyFinish.materialPreset,
|
|
202
|
-
};
|
|
203
224
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
// Turn a legacy surface spec (`{ material, materialPreset }`) into a
|
|
228
|
+
// `MaterialRef`: a preset that's already a `library:`/`scene:` ref is used
|
|
229
|
+
// as-is; an inline material mints (or reuses) a scene material. Returns
|
|
230
|
+
// undefined when the spec carries no material. Shared by every legacy→slots
|
|
231
|
+
// migration below.
|
|
232
|
+
function legacySpecToMaterialRef(spec, mintedMaterials) {
|
|
233
|
+
if (typeof spec.materialPreset === 'string' && parseMaterialRef(spec.materialPreset)) {
|
|
234
|
+
return spec.materialPreset;
|
|
210
235
|
}
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
236
|
+
if (spec.material !== undefined) {
|
|
237
|
+
const existing = findMintedSceneMaterialRef(spec.material, mintedMaterials);
|
|
238
|
+
if (existing)
|
|
239
|
+
return existing;
|
|
240
|
+
const id = generateSceneMaterialId();
|
|
241
|
+
mintedMaterials[id] = {
|
|
242
|
+
id,
|
|
243
|
+
name: `Material ${Object.keys(mintedMaterials).length + 1}`,
|
|
244
|
+
material: spec.material,
|
|
216
245
|
};
|
|
246
|
+
return toSceneMaterialRef(id);
|
|
247
|
+
}
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
// Move the retired inline `material*` / `interiorMaterial*` / `exteriorMaterial*`
|
|
251
|
+
// fields onto the unified `node.slots` model (interior / exterior → a
|
|
252
|
+
// `library:`/`scene:` ref), minting scene materials for inline customs into
|
|
253
|
+
// `mintedMaterials` (merged into the scene material map by the caller). Already
|
|
254
|
+
// slot-modelled walls and walls with no legacy material are left untouched.
|
|
255
|
+
function migrateWallSurfaceMaterials(node, mintedMaterials) {
|
|
256
|
+
if (node.slots && (node.slots.interior !== undefined || node.slots.exterior !== undefined)) {
|
|
257
|
+
return node;
|
|
258
|
+
}
|
|
259
|
+
const slots = { ...(node.slots ?? {}) };
|
|
260
|
+
for (const side of ['interior', 'exterior']) {
|
|
261
|
+
const spec = getEffectiveWallSurfaceMaterial(node, side);
|
|
262
|
+
const ref = legacySpecToMaterialRef(spec, mintedMaterials);
|
|
263
|
+
if (ref)
|
|
264
|
+
slots[side] = ref;
|
|
265
|
+
}
|
|
266
|
+
if (Object.keys(slots).length === 0) {
|
|
267
|
+
return node;
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
...node,
|
|
271
|
+
slots,
|
|
272
|
+
material: undefined,
|
|
273
|
+
materialPreset: undefined,
|
|
274
|
+
interiorMaterial: undefined,
|
|
275
|
+
interiorMaterialPreset: undefined,
|
|
276
|
+
exteriorMaterial: undefined,
|
|
277
|
+
exteriorMaterialPreset: undefined,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
// Move a kind's single legacy `material` / `materialPreset` onto its declared
|
|
281
|
+
// slots. A pre-slot-model node painted one material rendered that material on
|
|
282
|
+
// every part (each slot resolves `node.slots[slot]` → legacy → default), so the
|
|
283
|
+
// migration writes the same ref to every slot id the kind can expose — unused
|
|
284
|
+
// conditional slots are harmless. Already slot-modelled or unpainted nodes are
|
|
285
|
+
// left untouched. Mirrors `migrateWallSurfaceMaterials` for single-surface and
|
|
286
|
+
// whole-object kinds (slab, ceiling, fence, column, shelf).
|
|
287
|
+
function migrateSingleMaterialSlots(node, slotIds, mintedMaterials) {
|
|
288
|
+
if (node.slots && Object.keys(node.slots).length > 0) {
|
|
289
|
+
return node;
|
|
290
|
+
}
|
|
291
|
+
const ref = legacySpecToMaterialRef({ material: node.material, materialPreset: node.materialPreset }, mintedMaterials);
|
|
292
|
+
if (!ref) {
|
|
293
|
+
return node;
|
|
294
|
+
}
|
|
295
|
+
const slots = {};
|
|
296
|
+
for (const slotId of slotIds)
|
|
297
|
+
slots[slotId] = ref;
|
|
298
|
+
return { ...node, slots, material: undefined, materialPreset: undefined };
|
|
299
|
+
}
|
|
300
|
+
// Stair carries per-role legacy fields (`treadMaterial*` / `sideMaterial*` /
|
|
301
|
+
// `railingMaterial*`) plus a catch-all. Map each to its slot via the same
|
|
302
|
+
// fallback chain the renderer uses (`getEffectiveStairSurfaceMaterial`):
|
|
303
|
+
// tread→treads, side→body, railing→railing. Runs after
|
|
304
|
+
// `migrateStairSurfaceMaterials` has normalised the legacy fields.
|
|
305
|
+
function migrateStairSurfaceSlots(node, mintedMaterials) {
|
|
306
|
+
if (node.slots && Object.keys(node.slots).length > 0) {
|
|
307
|
+
return node;
|
|
308
|
+
}
|
|
309
|
+
const roleToSlot = [
|
|
310
|
+
['tread', 'treads'],
|
|
311
|
+
['side', 'body'],
|
|
312
|
+
['railing', 'railing'],
|
|
313
|
+
];
|
|
314
|
+
const slots = {};
|
|
315
|
+
for (const [role, slotId] of roleToSlot) {
|
|
316
|
+
const spec = getEffectiveStairSurfaceMaterial(node, role);
|
|
317
|
+
const ref = legacySpecToMaterialRef(spec, mintedMaterials);
|
|
318
|
+
if (ref)
|
|
319
|
+
slots[slotId] = ref;
|
|
320
|
+
}
|
|
321
|
+
if (Object.keys(slots).length === 0) {
|
|
322
|
+
return node;
|
|
217
323
|
}
|
|
218
|
-
return
|
|
324
|
+
return {
|
|
325
|
+
...node,
|
|
326
|
+
slots,
|
|
327
|
+
material: undefined,
|
|
328
|
+
materialPreset: undefined,
|
|
329
|
+
treadMaterial: undefined,
|
|
330
|
+
treadMaterialPreset: undefined,
|
|
331
|
+
sideMaterial: undefined,
|
|
332
|
+
sideMaterialPreset: undefined,
|
|
333
|
+
railingMaterial: undefined,
|
|
334
|
+
railingMaterialPreset: undefined,
|
|
335
|
+
};
|
|
219
336
|
}
|
|
220
337
|
function migrateStairSurfaceMaterials(node) {
|
|
221
338
|
const hasRailing = node.railingMaterial !== undefined || typeof node.railingMaterialPreset === 'string';
|
|
@@ -333,8 +450,84 @@ function migrateRoofSurfaceMaterials(node) {
|
|
|
333
450
|
}
|
|
334
451
|
return next;
|
|
335
452
|
}
|
|
453
|
+
function migrateConstructionDimension(node) {
|
|
454
|
+
const drawingOverrides = Array.isArray(node.drawingOverrides) ? node.drawingOverrides : [];
|
|
455
|
+
const hasLegacyDrawingOverride = drawingOverrides.some((entry) => entry &&
|
|
456
|
+
typeof entry === 'object' &&
|
|
457
|
+
!Array.isArray(entry) &&
|
|
458
|
+
entry.presentation === 'reference');
|
|
459
|
+
if (!('reference' in node || 'referenceStyle' in node || hasLegacyDrawingOverride))
|
|
460
|
+
return node;
|
|
461
|
+
const { reference: _reference, referenceStyle: _referenceStyle, ...dimension } = node;
|
|
462
|
+
return {
|
|
463
|
+
...dimension,
|
|
464
|
+
drawingOverrides: drawingOverrides.map((entry) => {
|
|
465
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
466
|
+
return entry;
|
|
467
|
+
return entry.presentation === 'reference' ? { ...entry, presentation: 'shown' } : entry;
|
|
468
|
+
}),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function removeRetiredDrawingSheets(nodes) {
|
|
472
|
+
const retiredIds = new Set(Object.entries(nodes)
|
|
473
|
+
.filter(([, node]) => node?.type === 'drawing-sheet')
|
|
474
|
+
.map(([id]) => id));
|
|
475
|
+
if (retiredIds.size === 0)
|
|
476
|
+
return;
|
|
477
|
+
for (const id of retiredIds)
|
|
478
|
+
delete nodes[id];
|
|
479
|
+
for (const [id, node] of Object.entries(nodes)) {
|
|
480
|
+
if (!Array.isArray(node?.children))
|
|
481
|
+
continue;
|
|
482
|
+
const children = getStringArray(node.children);
|
|
483
|
+
if (!children.some((childId) => retiredIds.has(childId)))
|
|
484
|
+
continue;
|
|
485
|
+
nodes[id] = {
|
|
486
|
+
...node,
|
|
487
|
+
children: children.filter((childId) => !retiredIds.has(childId)),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function migrateWallAssembly(node) {
|
|
492
|
+
if (!Object.hasOwn(node, 'assemblyLayers'))
|
|
493
|
+
return node;
|
|
494
|
+
const assemblyThickness = Array.isArray(node.assemblyLayers)
|
|
495
|
+
? node.assemblyLayers.reduce((total, layer) => {
|
|
496
|
+
if (!(layer && typeof layer === 'object'))
|
|
497
|
+
return total;
|
|
498
|
+
const thickness = layer.thickness;
|
|
499
|
+
return typeof thickness === 'number' && Number.isFinite(thickness) && thickness > 0
|
|
500
|
+
? total + thickness
|
|
501
|
+
: total;
|
|
502
|
+
}, 0)
|
|
503
|
+
: 0;
|
|
504
|
+
const { assemblyLayers: _assemblyLayers, ...wall } = node;
|
|
505
|
+
return assemblyThickness > 0 ? { ...wall, thickness: assemblyThickness } : wall;
|
|
506
|
+
}
|
|
507
|
+
// Walls whose top lands within this of the storey plane become plane-bound;
|
|
508
|
+
// ceilings whose stored height lands within this of their clamp bound become
|
|
509
|
+
// follows-mode (step 3f) — same census-backed threshold for both.
|
|
510
|
+
// From a prod census: the 0.15-short "hole pattern" (default 2.5 walls next to
|
|
511
|
+
// a taller wall) must snap to the plane, while intentional 0.20-short walls
|
|
512
|
+
// (2.5 under a 2.7 plane, 2.3 under a 2.5 plane) must keep their explicit
|
|
513
|
+
// height — hence 0.20 with a strictly-less-than comparison.
|
|
514
|
+
const PLANE_BOUND_EPSILON = 0.2;
|
|
336
515
|
function migrateNodes(nodes) {
|
|
337
|
-
|
|
516
|
+
// Repair pre-existing corruption (null children, zero-length walls) before
|
|
517
|
+
// any per-type migration runs, so already-saved scenes load cleanly.
|
|
518
|
+
const { nodes: healed } = healSceneNodes(nodes);
|
|
519
|
+
const patchedNodes = { ...healed };
|
|
520
|
+
removeRetiredDrawingSheets(patchedNodes);
|
|
521
|
+
// Scene materials minted while moving legacy wall fields onto `node.slots`;
|
|
522
|
+
// merged into the scene material map by the caller (`setScene`).
|
|
523
|
+
const mintedMaterials = {};
|
|
524
|
+
// Pass 1: all node types except elevator.
|
|
525
|
+
// Elevator migration (migrateElevatorParent) mutates level.children to remove
|
|
526
|
+
// the elevator ID. If the elevator is processed before its parent level in
|
|
527
|
+
// Object.entries order, the level migration in this same pass would then see
|
|
528
|
+
// a children array that still contains the elevator ID and filter it out as
|
|
529
|
+
// "missing" — corrupting the level. Running elevators in a second pass after
|
|
530
|
+
// all levels are stable avoids the race entirely.
|
|
338
531
|
for (const [id, node] of Object.entries(patchedNodes)) {
|
|
339
532
|
// 1. Item scale migration
|
|
340
533
|
if (node.type === 'item' && !('scale' in node)) {
|
|
@@ -410,11 +603,21 @@ function migrateNodes(nodes) {
|
|
|
410
603
|
patchedNodes[id] = normalized;
|
|
411
604
|
}
|
|
412
605
|
}
|
|
606
|
+
if (node.type === 'window') {
|
|
607
|
+
const normalized = normalizeWindowNode(node);
|
|
608
|
+
if (normalized) {
|
|
609
|
+
patchedNodes[id] = normalized;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (node.type === 'construction-dimension') {
|
|
613
|
+
patchedNodes[id] = migrateConstructionDimension(node);
|
|
614
|
+
}
|
|
413
615
|
if (node.type === 'stair') {
|
|
414
616
|
const normalized = normalizeStairNode(migrateStairSurfaceMaterials(node));
|
|
415
617
|
if (normalized) {
|
|
416
618
|
patchedNodes[id] = normalized;
|
|
417
619
|
}
|
|
620
|
+
patchedNodes[id] = migrateStairSurfaceSlots(patchedNodes[id], mintedMaterials);
|
|
418
621
|
}
|
|
419
622
|
if (node.type === 'stair-segment') {
|
|
420
623
|
const normalized = normalizeStairSegmentNode(node);
|
|
@@ -423,20 +626,52 @@ function migrateNodes(nodes) {
|
|
|
423
626
|
}
|
|
424
627
|
}
|
|
425
628
|
if (node.type === 'wall') {
|
|
426
|
-
patchedNodes[id] = migrateWallSurfaceMaterials(patchedNodes[id]);
|
|
629
|
+
patchedNodes[id] = migrateWallSurfaceMaterials(migrateWallAssembly(patchedNodes[id]), mintedMaterials);
|
|
427
630
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
631
|
+
// Cabinet v2→v3: node-level `doorStyle` was dead (geometry reads only the
|
|
632
|
+
// per-compartment `doorType`) and was removed from both cabinet schemas;
|
|
633
|
+
// `handlePosition: 'edge'` behaved identically to 'auto' and was dropped
|
|
634
|
+
// from the enum; the compartment stack became a discriminated union that
|
|
635
|
+
// rejects a `cooktopLayout` mismatched to its gas/induction type (the old
|
|
636
|
+
// loose schema ignored it).
|
|
637
|
+
if (node.type === 'cabinet' || node.type === 'cabinet-module') {
|
|
638
|
+
const { doorStyle: _doorStyle, ...rest } = node;
|
|
639
|
+
const next = rest;
|
|
640
|
+
if (next.handlePosition === 'edge')
|
|
641
|
+
next.handlePosition = 'auto';
|
|
642
|
+
if (Array.isArray(next.stack)) {
|
|
643
|
+
next.stack = next.stack.map((compartment) => {
|
|
644
|
+
if (!compartment || typeof compartment !== 'object')
|
|
645
|
+
return compartment;
|
|
646
|
+
const layout = compartment.cooktopLayout;
|
|
647
|
+
if (typeof layout !== 'string')
|
|
648
|
+
return compartment;
|
|
649
|
+
if (compartment.type === 'cooktop-gas' && !layout.startsWith('gas-')) {
|
|
650
|
+
return { ...compartment, cooktopLayout: 'gas-4burner' };
|
|
651
|
+
}
|
|
652
|
+
if (compartment.type === 'cooktop-induction' && !layout.startsWith('induction-')) {
|
|
653
|
+
return { ...compartment, cooktopLayout: 'induction-4zone' };
|
|
654
|
+
}
|
|
655
|
+
return compartment;
|
|
656
|
+
});
|
|
432
657
|
}
|
|
658
|
+
patchedNodes[id] = next;
|
|
659
|
+
}
|
|
660
|
+
if (node.type === 'slab' || node.type === 'ceiling') {
|
|
661
|
+
patchedNodes[id] = migrateSingleMaterialSlots(patchedNodes[id], ['surface'], mintedMaterials);
|
|
662
|
+
}
|
|
663
|
+
if (node.type === 'fence') {
|
|
664
|
+
patchedNodes[id] = migrateSingleMaterialSlots(patchedNodes[id], ['posts', 'infill', 'base', 'rail'], mintedMaterials);
|
|
665
|
+
}
|
|
666
|
+
if (node.type === 'column') {
|
|
667
|
+
patchedNodes[id] = migrateSingleMaterialSlots(patchedNodes[id], ['shaft', 'base', 'capital', 'frame'], mintedMaterials);
|
|
433
668
|
}
|
|
434
|
-
if (node.type === '
|
|
435
|
-
const
|
|
436
|
-
const normalized = normalizeElevatorNode(parentMigrated);
|
|
669
|
+
if (node.type === 'shelf') {
|
|
670
|
+
const normalized = normalizeShelfNode(node);
|
|
437
671
|
if (normalized) {
|
|
438
672
|
patchedNodes[id] = normalized;
|
|
439
673
|
}
|
|
674
|
+
patchedNodes[id] = migrateSingleMaterialSlots(patchedNodes[id], ['shelves', 'frame', 'back'], mintedMaterials);
|
|
440
675
|
}
|
|
441
676
|
// Roof-segment hosting was added in this migration cycle (the same
|
|
442
677
|
// pattern as shelf above). Older segments saved before the schema
|
|
@@ -449,6 +684,41 @@ function migrateNodes(nodes) {
|
|
|
449
684
|
if (node.type === 'roof-segment' && !Array.isArray(node.children)) {
|
|
450
685
|
patchedNodes[id] = { ...node, children: [] };
|
|
451
686
|
}
|
|
687
|
+
// Roof-hosted wall children (door / window / item) originally stored
|
|
688
|
+
// SEGMENT-LOCAL positions with the face yaw in rotation[1]; the
|
|
689
|
+
// format moved to explicit `roofFace` + FACE-LOCAL coords so the
|
|
690
|
+
// renderer's face frame can track segment edits live. Convert in
|
|
691
|
+
// place: face from the old cardinal yaw, u/v from the outer-plane
|
|
692
|
+
// projection, z re-based from the outer plane to the wall mid-plane.
|
|
693
|
+
if ((node.type === 'door' || node.type === 'window' || node.type === 'item') &&
|
|
694
|
+
typeof node.roofSegmentId === 'string' &&
|
|
695
|
+
node.roofFace === undefined) {
|
|
696
|
+
const current = patchedNodes[id];
|
|
697
|
+
const segment = patchedNodes[current.roofSegmentId];
|
|
698
|
+
if (segment?.type === 'roof-segment') {
|
|
699
|
+
const tau = Math.PI * 2;
|
|
700
|
+
const yaw = (((current.rotation?.[1] ?? 0) % tau) + tau) % tau;
|
|
701
|
+
const eps = 1e-3;
|
|
702
|
+
const face = yaw < eps || tau - yaw < eps
|
|
703
|
+
? 'front'
|
|
704
|
+
: Math.abs(yaw - Math.PI) < eps
|
|
705
|
+
? 'back'
|
|
706
|
+
: Math.abs(yaw - Math.PI / 2) < eps
|
|
707
|
+
? 'right'
|
|
708
|
+
: Math.abs(yaw - (3 * Math.PI) / 2) < eps
|
|
709
|
+
? 'left'
|
|
710
|
+
: null;
|
|
711
|
+
if (face) {
|
|
712
|
+
const { u, v, dist } = segmentPointToRoofWallFace(segment, face, current.position);
|
|
713
|
+
patchedNodes[id] = {
|
|
714
|
+
...current,
|
|
715
|
+
roofFace: face,
|
|
716
|
+
position: [u, v, dist + (segment.wallThickness ?? 0.1) / 2],
|
|
717
|
+
rotation: [0, 0, 0],
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
452
722
|
if (node.type === 'roof') {
|
|
453
723
|
patchedNodes[id] = migrateRoofSurfaceMaterials(patchedNodes[id]);
|
|
454
724
|
}
|
|
@@ -476,8 +746,200 @@ function migrateNodes(nodes) {
|
|
|
476
746
|
patchedNodes[id] = { ...node, children: flattened };
|
|
477
747
|
}
|
|
478
748
|
}
|
|
749
|
+
// Level children normalization.
|
|
750
|
+
// Pre-0.9.1 JSONs may carry child IDs that no longer exist in the node
|
|
751
|
+
// map (e.g. elevator IDs that lived under a level before the elevator
|
|
752
|
+
// parent migration moved them up to building). If those dangling IDs are
|
|
753
|
+
// left in place, collectReachableNodeIds marks the level as having
|
|
754
|
+
// reachable children that don't exist, which corrupts the scene graph
|
|
755
|
+
// traversal and leaves the LevelNode in a broken state — making floors
|
|
756
|
+
// impossible to drag or delete after import.
|
|
757
|
+
// We intentionally do NOT filter by type prefix here; being permissive
|
|
758
|
+
// about which types are allowed as children prevents data loss when new
|
|
759
|
+
// child types are added to the schema in the future.
|
|
760
|
+
if (node.type === 'level') {
|
|
761
|
+
const rawChildren = getStringArray(node.children);
|
|
762
|
+
const validChildren = rawChildren.filter((childId) => {
|
|
763
|
+
const exists = Boolean(patchedNodes[childId]);
|
|
764
|
+
if (!exists) {
|
|
765
|
+
console.warn('[migrateNodes] level', id, 'references missing child', childId, '— dropping');
|
|
766
|
+
}
|
|
767
|
+
return exists;
|
|
768
|
+
});
|
|
769
|
+
const levelNumber = getFiniteNumber(node.level, 0);
|
|
770
|
+
patchedNodes[id] = {
|
|
771
|
+
...node,
|
|
772
|
+
level: levelNumber,
|
|
773
|
+
children: validChildren,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
// Pass 2: elevator migration.
|
|
778
|
+
// migrateElevatorParent mutates the parent level's children array (removes
|
|
779
|
+
// the elevator ID from it). Running this after Pass 1 guarantees that the
|
|
780
|
+
// level normalization above has already seen a clean children list — if we
|
|
781
|
+
// ran elevator migration inside Pass 1, the order of Object.entries
|
|
782
|
+
// iteration would be non-deterministic: processing an elevator before its
|
|
783
|
+
// parent level would mutate the level's children mid-iteration, potentially
|
|
784
|
+
// causing the level branch above to see a stale node reference.
|
|
785
|
+
for (const [id, node] of Object.entries(patchedNodes)) {
|
|
786
|
+
if (node.type !== 'elevator')
|
|
787
|
+
continue;
|
|
788
|
+
const parentMigrated = migrateElevatorParent(id, node, patchedNodes);
|
|
789
|
+
const normalized = normalizeElevatorNode(parentMigrated);
|
|
790
|
+
if (normalized) {
|
|
791
|
+
patchedNodes[id] = normalized;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// Pass 3: vertical building model.
|
|
795
|
+
// A level without `height` marks a scene saved before the vertical model
|
|
796
|
+
// landed. Computed before this pass mutates anything: the stair-rise
|
|
797
|
+
// cleanup below must never run on already-migrated scenes.
|
|
798
|
+
const isLegacyScene = Object.values(patchedNodes).some((node) => node?.type === 'level' && !('height' in node));
|
|
799
|
+
// 3a. Ordinal renumber — always runs, per building (idempotent
|
|
800
|
+
// self-healing; MCP's create-level historically wrote its elevation PARAM
|
|
801
|
+
// into the ordinal, so fractional/duplicate ordinals exist in the wild).
|
|
802
|
+
const buildingNodes = Object.values(patchedNodes).filter((node) => node?.type === 'building');
|
|
803
|
+
const levelsByBuilding = new Map();
|
|
804
|
+
for (const [id, node] of Object.entries(patchedNodes)) {
|
|
805
|
+
if (node?.type !== 'level')
|
|
806
|
+
continue;
|
|
807
|
+
// Mirrors the building resolution in services/storey.ts: an explicit
|
|
808
|
+
// parentId pointing at a building wins, membership in a building's
|
|
809
|
+
// children array is the legacy fallback, and unresolvable levels share
|
|
810
|
+
// one orphan bucket.
|
|
811
|
+
const buildingId = buildingNodes.find((building) => building.id === node.parentId)?.id ??
|
|
812
|
+
buildingNodes.find((building) => getStringArray(building.children).includes(id))?.id ??
|
|
813
|
+
null;
|
|
814
|
+
const bucket = levelsByBuilding.get(buildingId) ?? [];
|
|
815
|
+
bucket.push({ id, ordinal: getFiniteNumber(node.level, 0) });
|
|
816
|
+
levelsByBuilding.set(buildingId, bucket);
|
|
817
|
+
}
|
|
818
|
+
for (const bucket of levelsByBuilding.values()) {
|
|
819
|
+
// Anchored at zero on purpose: ordinals are semantic — `level < 0`
|
|
820
|
+
// renders "Basement N" and `level === 0` is the ground-floor default —
|
|
821
|
+
// so negatives compact upward toward −1 and non-negatives compact down
|
|
822
|
+
// to 0. A blind 0..n renumber would rename basements.
|
|
823
|
+
const sorted = [...bucket].sort((a, b) => a.ordinal - b.ordinal);
|
|
824
|
+
const negativeCount = sorted.filter((entry) => entry.ordinal < 0).length;
|
|
825
|
+
sorted.forEach((entry, index) => {
|
|
826
|
+
const nextOrdinal = index - negativeCount;
|
|
827
|
+
const current = patchedNodes[entry.id];
|
|
828
|
+
if (current.level !== nextOrdinal) {
|
|
829
|
+
patchedNodes[entry.id] = { ...current, level: nextOrdinal };
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
// 3b. Stored storey heights: materialize the legacy stacked height verbatim
|
|
834
|
+
// (never rounded or snapped — snapping would move existing buildings).
|
|
835
|
+
// All planes derive before any wall height below mutates.
|
|
836
|
+
const legacyLevelIds = Object.entries(patchedNodes)
|
|
837
|
+
.filter(([, node]) => node?.type === 'level' && !('height' in node))
|
|
838
|
+
.map(([id]) => id);
|
|
839
|
+
const derivedHeights = new Map();
|
|
840
|
+
for (const levelId of legacyLevelIds) {
|
|
841
|
+
derivedHeights.set(levelId, deriveLegacyLevelHeight(levelId, patchedNodes));
|
|
842
|
+
}
|
|
843
|
+
for (const levelId of legacyLevelIds) {
|
|
844
|
+
const plane = derivedHeights.get(levelId);
|
|
845
|
+
const level = patchedNodes[levelId];
|
|
846
|
+
patchedNodes[levelId] = { ...level, height: plane };
|
|
847
|
+
// 3c. Wall-top classification against the just-written plane, using the
|
|
848
|
+
// same slab-support election as deriveLegacyLevelHeight (call shape
|
|
849
|
+
// mirrored from services/level-height.ts). Walls whose top meets the
|
|
850
|
+
// plane drop their explicit height and follow the level from now on;
|
|
851
|
+
// walls ending short (or tall) keep an explicit height — materializing
|
|
852
|
+
// the 2.5 default onto absent-height walls that end short of the plane.
|
|
853
|
+
const children = getStringArray(level.children)
|
|
854
|
+
.map((childId) => patchedNodes[childId])
|
|
855
|
+
.filter((child) => child !== undefined);
|
|
856
|
+
const slabs = children.filter((child) => child.type === 'slab');
|
|
857
|
+
const walls = children.filter((child) => child.type === 'wall');
|
|
858
|
+
for (const wall of walls) {
|
|
859
|
+
const electedBase = computeWallSlabSupport({
|
|
860
|
+
start: wall.start,
|
|
861
|
+
end: wall.end,
|
|
862
|
+
curveOffset: wall.curveOffset,
|
|
863
|
+
thickness: wall.thickness,
|
|
864
|
+
}, slabs, walls).elevation;
|
|
865
|
+
const effectiveHeight = wall.height ?? DEFAULT_WALL_HEIGHT;
|
|
866
|
+
const top = Math.max(0, electedBase) + effectiveHeight;
|
|
867
|
+
if (Math.abs(plane - top) < PLANE_BOUND_EPSILON) {
|
|
868
|
+
if ('height' in wall) {
|
|
869
|
+
const { height: _height, ...planeBound } = wall;
|
|
870
|
+
patchedNodes[wall.id] = planeBound;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
patchedNodes[wall.id] = { ...wall, height: effectiveHeight };
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
// 3d. Stair rise: on legacy scenes a totalRise of exactly 2.5 is the old
|
|
879
|
+
// schema default, not a user choice — drop it so the rise derives from the
|
|
880
|
+
// storey height. Gated on isLegacyScene because on a post-migration scene
|
|
881
|
+
// a stored 2.5 IS a deliberately typed value and must survive reloads.
|
|
882
|
+
if (isLegacyScene) {
|
|
883
|
+
for (const [id, node] of Object.entries(patchedNodes)) {
|
|
884
|
+
if (node?.type !== 'stair')
|
|
885
|
+
continue;
|
|
886
|
+
if (node.totalRise !== 2.5)
|
|
887
|
+
continue;
|
|
888
|
+
const { totalRise: _totalRise, ...derivedRise } = node;
|
|
889
|
+
patchedNodes[id] = derivedRise;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
// 3e. Slab placement/thickness split. `elevation` stays the walking surface;
|
|
893
|
+
// the new `thickness` grows downward so the solid occupies
|
|
894
|
+
// [elevation − thickness, elevation]. Legacy solids extruded [0, elevation],
|
|
895
|
+
// so thickness = elevation EXACTLY (including degenerate 0 — MIN_SLAB_THICKNESS
|
|
896
|
+
// applies to edits only, never here) keeps the occupied interval identical.
|
|
897
|
+
// Legacy pools (elevation < 0) become explicit `recessed` intent with
|
|
898
|
+
// elevation unchanged. Gated per slab on a missing `thickness` — the
|
|
899
|
+
// migration output is cast, so schema defaults never materialize on load.
|
|
900
|
+
for (const [id, node] of Object.entries(patchedNodes)) {
|
|
901
|
+
if (node?.type !== 'slab' || 'thickness' in node)
|
|
902
|
+
continue;
|
|
903
|
+
const elevation = getFiniteNumber(node.elevation, 0.05);
|
|
904
|
+
patchedNodes[id] =
|
|
905
|
+
elevation < 0
|
|
906
|
+
? { ...node, thickness: 0.05, recessed: true }
|
|
907
|
+
: { ...node, thickness: elevation };
|
|
908
|
+
}
|
|
909
|
+
// 3f. Ceiling follows-mode classification (the ceiling mirror of 3c; runs
|
|
910
|
+
// after 3b/3e so the clamp bound sees stored level heights and split slab
|
|
911
|
+
// thicknesses). A stored ceiling height within PLANE_BOUND_EPSILON of its
|
|
912
|
+
// clamp bound (min(storey plane, covering-slab underside) − margin, via
|
|
913
|
+
// getCeilingClampBound) is the legacy default tracking the level top, not
|
|
914
|
+
// a choice — drop it so the ceiling follows the level from now on.
|
|
915
|
+
// autoFromWalls ceilings always convert: their height was derived by the
|
|
916
|
+
// space-detection sync, never user intent. Gated on isLegacyScene, which
|
|
917
|
+
// is exact — nothing shipped between the level-height migration and this
|
|
918
|
+
// one — and makes the step idempotent. Known accepted edge: a
|
|
919
|
+
// post-migration user typing a custom height exactly equal to the bound
|
|
920
|
+
// keeps it (the gate prevents re-classification on later loads).
|
|
921
|
+
if (isLegacyScene) {
|
|
922
|
+
for (const [id, node] of Object.entries(patchedNodes)) {
|
|
923
|
+
if (node?.type !== 'ceiling' || !('height' in node))
|
|
924
|
+
continue;
|
|
925
|
+
const dropHeight = () => {
|
|
926
|
+
const { height: _height, ...follows } = node;
|
|
927
|
+
patchedNodes[id] = follows;
|
|
928
|
+
};
|
|
929
|
+
if (node.autoFromWalls === true) {
|
|
930
|
+
dropHeight();
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (typeof node.parentId !== 'string')
|
|
934
|
+
continue;
|
|
935
|
+
const bound = getCeilingClampBound(node.parentId, patchedNodes, Array.isArray(node.polygon) ? node.polygon : []);
|
|
936
|
+
const stored = getFiniteNumber(node.height, Number.NaN);
|
|
937
|
+
if (Number.isFinite(bound) && Math.abs(stored - bound) < PLANE_BOUND_EPSILON) {
|
|
938
|
+
dropHeight();
|
|
939
|
+
}
|
|
940
|
+
}
|
|
479
941
|
}
|
|
480
|
-
return patchedNodes;
|
|
942
|
+
return { nodes: patchedNodes, mintedMaterials };
|
|
481
943
|
}
|
|
482
944
|
function getNodeChildIds(node) {
|
|
483
945
|
if (!('children' in node && Array.isArray(node.children))) {
|
|
@@ -527,6 +989,10 @@ function collectReachableNodeIds(nodes, rootNodeIds) {
|
|
|
527
989
|
}
|
|
528
990
|
return reachable;
|
|
529
991
|
}
|
|
992
|
+
function sceneHistorySnapshotFromState(state) {
|
|
993
|
+
const { nodes, rootNodeIds, collections, materials, installedPlugins } = state;
|
|
994
|
+
return { nodes, rootNodeIds, collections, materials, installedPlugins };
|
|
995
|
+
}
|
|
530
996
|
const useScene = create()(temporal((set, get) => ({
|
|
531
997
|
// 1. Flat dictionary of all nodes
|
|
532
998
|
nodes: {},
|
|
@@ -536,6 +1002,9 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
536
1002
|
dirtyNodes: new Set(),
|
|
537
1003
|
// 4. Collections
|
|
538
1004
|
collections: {},
|
|
1005
|
+
materials: {},
|
|
1006
|
+
installedPlugins: [],
|
|
1007
|
+
hasExplicitPluginInstallState: false,
|
|
539
1008
|
// 5. Read-only lock
|
|
540
1009
|
readOnly: false,
|
|
541
1010
|
setReadOnly: (readOnly) => set({ readOnly }),
|
|
@@ -545,15 +1014,25 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
545
1014
|
rootNodeIds: [],
|
|
546
1015
|
dirtyNodes: new Set(),
|
|
547
1016
|
collections: {},
|
|
1017
|
+
materials: {},
|
|
1018
|
+
installedPlugins: [],
|
|
1019
|
+
hasExplicitPluginInstallState: false,
|
|
548
1020
|
});
|
|
549
1021
|
},
|
|
550
1022
|
clearScene: () => {
|
|
1023
|
+
const installedPlugins = get().installedPlugins;
|
|
1024
|
+
const hasExplicitPluginInstallState = get().hasExplicitPluginInstallState;
|
|
551
1025
|
get().unloadScene();
|
|
552
1026
|
get().loadScene(); // Default scene
|
|
1027
|
+
set({ installedPlugins, hasExplicitPluginInstallState });
|
|
553
1028
|
},
|
|
554
|
-
setScene: (nodes, rootNodeIds) => {
|
|
1029
|
+
setScene: (nodes, rootNodeIds, extra) => {
|
|
555
1030
|
// Apply backward compatibility migrations
|
|
556
|
-
const patchedNodes = migrateNodes(nodes);
|
|
1031
|
+
const { nodes: patchedNodes, mintedMaterials } = migrateNodes(nodes);
|
|
1032
|
+
// Scene materials minted by the wall legacy→slots migration join the
|
|
1033
|
+
// loaded palette (existing refs win on id collision — there are none,
|
|
1034
|
+
// ids are freshly generated).
|
|
1035
|
+
const materials = { ...mintedMaterials, ...(extra?.materials ?? {}) };
|
|
557
1036
|
// Remove orphans: nodes whose parentId points to a non-existent node
|
|
558
1037
|
const cleanedNodes = { ...patchedNodes };
|
|
559
1038
|
for (const node of Object.values(cleanedNodes)) {
|
|
@@ -562,12 +1041,6 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
562
1041
|
delete cleanedNodes[node.id];
|
|
563
1042
|
}
|
|
564
1043
|
}
|
|
565
|
-
set({
|
|
566
|
-
nodes: cleanedNodes,
|
|
567
|
-
rootNodeIds,
|
|
568
|
-
dirtyNodes: new Set(),
|
|
569
|
-
collections: {},
|
|
570
|
-
});
|
|
571
1044
|
const normalizedRootNodeIds = normalizeRootNodeIds(cleanedNodes, rootNodeIds);
|
|
572
1045
|
const reachableNodeIds = collectReachableNodeIds(cleanedNodes, normalizedRootNodeIds);
|
|
573
1046
|
if (normalizedRootNodeIds.length > 0) {
|
|
@@ -578,17 +1051,47 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
578
1051
|
delete cleanedNodes[node.id];
|
|
579
1052
|
}
|
|
580
1053
|
}
|
|
1054
|
+
// Single tracked `set`: with zundo, every tracked write pushes the
|
|
1055
|
+
// pre-write state onto `pastStates`. Writing the scene in two steps
|
|
1056
|
+
// (as this used to) exposed a half-normalized intermediate state —
|
|
1057
|
+
// and the pre-load (possibly empty) state — as undo targets.
|
|
581
1058
|
set({
|
|
582
1059
|
nodes: cleanedNodes,
|
|
583
1060
|
rootNodeIds: normalizedRootNodeIds,
|
|
584
1061
|
dirtyNodes: new Set(),
|
|
585
|
-
collections: {},
|
|
1062
|
+
collections: extra?.collections ?? {},
|
|
1063
|
+
materials,
|
|
1064
|
+
installedPlugins: Array.from(new Set(extra?.installedPlugins ?? [])),
|
|
1065
|
+
hasExplicitPluginInstallState: extra?.hasExplicitPluginInstallState ?? false,
|
|
586
1066
|
});
|
|
587
1067
|
// Mark all nodes as dirty to trigger re-validation
|
|
588
1068
|
Object.values(cleanedNodes).forEach((node) => {
|
|
589
1069
|
get().markDirty(node.id);
|
|
590
1070
|
});
|
|
591
1071
|
},
|
|
1072
|
+
setInstalledPlugins: (pluginIds, options) => {
|
|
1073
|
+
if (get().readOnly)
|
|
1074
|
+
return;
|
|
1075
|
+
const nextInstalledPlugins = Array.from(new Set(pluginIds));
|
|
1076
|
+
const previousInstalledPlugins = get().installedPlugins;
|
|
1077
|
+
const dirtyNodes = new Set(get().dirtyNodes);
|
|
1078
|
+
for (const node of Object.values(get().nodes)) {
|
|
1079
|
+
if (!getNodePluginId(node.type))
|
|
1080
|
+
continue;
|
|
1081
|
+
if (!isNodeKindEnabled(node.type, nextInstalledPlugins)) {
|
|
1082
|
+
dirtyNodes.delete(node.id);
|
|
1083
|
+
}
|
|
1084
|
+
else if (!isNodeKindEnabled(node.type, previousInstalledPlugins)) {
|
|
1085
|
+
if (nodeRegistry.get(node.type)?.dirtyTracking !== false)
|
|
1086
|
+
dirtyNodes.add(node.id);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
set({
|
|
1090
|
+
installedPlugins: nextInstalledPlugins,
|
|
1091
|
+
hasExplicitPluginInstallState: options?.explicit ?? get().hasExplicitPluginInstallState,
|
|
1092
|
+
dirtyNodes,
|
|
1093
|
+
});
|
|
1094
|
+
},
|
|
592
1095
|
loadScene: () => {
|
|
593
1096
|
if (get().rootNodeIds.length > 0) {
|
|
594
1097
|
// Assign all nodes as dirty to force re-validation
|
|
@@ -601,6 +1104,7 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
601
1104
|
const level0 = LevelNode.parse({
|
|
602
1105
|
level: 0,
|
|
603
1106
|
children: [],
|
|
1107
|
+
height: 2.5,
|
|
604
1108
|
});
|
|
605
1109
|
const building = BuildingNode.parse({
|
|
606
1110
|
children: [level0.id],
|
|
@@ -619,6 +1123,11 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
619
1123
|
set({ nodes, rootNodeIds });
|
|
620
1124
|
},
|
|
621
1125
|
markDirty: (id) => {
|
|
1126
|
+
const node = get().nodes[id];
|
|
1127
|
+
if (node && !isNodeKindEnabled(node.type, get().installedPlugins))
|
|
1128
|
+
return;
|
|
1129
|
+
if (node && nodeRegistry.get(node.type)?.dirtyTracking === false)
|
|
1130
|
+
return;
|
|
622
1131
|
get().dirtyNodes.add(id);
|
|
623
1132
|
},
|
|
624
1133
|
clearDirty: (id) => {
|
|
@@ -730,20 +1239,448 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
730
1239
|
return { collections: nextCollections, nodes: nextNodes };
|
|
731
1240
|
});
|
|
732
1241
|
},
|
|
1242
|
+
// --- SCENE MATERIALS ---
|
|
1243
|
+
addSceneMaterial: (material) => {
|
|
1244
|
+
if (get().readOnly)
|
|
1245
|
+
return;
|
|
1246
|
+
set((state) => ({
|
|
1247
|
+
materials: { ...state.materials, [material.id]: material },
|
|
1248
|
+
}));
|
|
1249
|
+
},
|
|
1250
|
+
updateSceneMaterial: (id, data) => {
|
|
1251
|
+
if (get().readOnly)
|
|
1252
|
+
return;
|
|
1253
|
+
set((state) => {
|
|
1254
|
+
const material = state.materials[id];
|
|
1255
|
+
if (!material)
|
|
1256
|
+
return state;
|
|
1257
|
+
return { materials: { ...state.materials, [id]: { ...material, ...data } } };
|
|
1258
|
+
});
|
|
1259
|
+
},
|
|
1260
|
+
removeSceneMaterial: (id) => {
|
|
1261
|
+
if (get().readOnly)
|
|
1262
|
+
return;
|
|
1263
|
+
set((state) => {
|
|
1264
|
+
const materials = { ...state.materials };
|
|
1265
|
+
delete materials[id];
|
|
1266
|
+
return { materials };
|
|
1267
|
+
});
|
|
1268
|
+
},
|
|
733
1269
|
}), {
|
|
734
|
-
partialize: (state) =>
|
|
735
|
-
|
|
736
|
-
|
|
1270
|
+
partialize: (state) => sceneHistorySnapshotFromState(state),
|
|
1271
|
+
equality: (pastState, currentState) => areSceneSnapshotsEqual(pastState, currentState),
|
|
1272
|
+
onSave: (pastState, currentState) => {
|
|
1273
|
+
notifySceneCommit({
|
|
1274
|
+
origin: 'local',
|
|
1275
|
+
before: sceneHistorySnapshotFromState(pastState),
|
|
1276
|
+
current: sceneHistorySnapshotFromState(currentState),
|
|
1277
|
+
});
|
|
737
1278
|
},
|
|
738
1279
|
limit: 50, // Limit to last 50 actions
|
|
739
1280
|
}));
|
|
740
1281
|
export default useScene;
|
|
1282
|
+
let sceneReadOnlyLeaseCount = 0;
|
|
1283
|
+
let sceneReadOnlyLeaseBaseline = false;
|
|
1284
|
+
export function acquireSceneReadOnlyLease() {
|
|
1285
|
+
if (sceneReadOnlyLeaseCount === 0) {
|
|
1286
|
+
sceneReadOnlyLeaseBaseline = useScene.getState().readOnly;
|
|
1287
|
+
}
|
|
1288
|
+
sceneReadOnlyLeaseCount += 1;
|
|
1289
|
+
useScene.setState({ readOnly: true });
|
|
1290
|
+
let released = false;
|
|
1291
|
+
return () => {
|
|
1292
|
+
if (released)
|
|
1293
|
+
return;
|
|
1294
|
+
released = true;
|
|
1295
|
+
sceneReadOnlyLeaseCount = Math.max(0, sceneReadOnlyLeaseCount - 1);
|
|
1296
|
+
if (sceneReadOnlyLeaseCount > 0)
|
|
1297
|
+
return;
|
|
1298
|
+
useScene.setState({ readOnly: sceneReadOnlyLeaseBaseline });
|
|
1299
|
+
sceneReadOnlyLeaseBaseline = false;
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
function sceneOperationPatchLiveConflictIds(beforeState, changes) {
|
|
1303
|
+
const ids = new Set();
|
|
1304
|
+
const addNodeAndParent = (node) => {
|
|
1305
|
+
if (!node)
|
|
1306
|
+
return;
|
|
1307
|
+
ids.add(node.id);
|
|
1308
|
+
if (node.parentId)
|
|
1309
|
+
ids.add(node.parentId);
|
|
1310
|
+
};
|
|
1311
|
+
for (const { id, data } of changes.nodeUpdates) {
|
|
1312
|
+
ids.add(id);
|
|
1313
|
+
if (Object.hasOwn(data, 'parentId')) {
|
|
1314
|
+
const currentParentId = beforeState.nodes[id]?.parentId;
|
|
1315
|
+
if (currentParentId)
|
|
1316
|
+
ids.add(currentParentId);
|
|
1317
|
+
if (typeof data.parentId === 'string')
|
|
1318
|
+
ids.add(data.parentId);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
for (const { node } of changes.nodeCreates)
|
|
1322
|
+
addNodeAndParent(node);
|
|
1323
|
+
for (const { node } of changes.nodeDeletes)
|
|
1324
|
+
addNodeAndParent(node);
|
|
1325
|
+
return ids;
|
|
1326
|
+
}
|
|
1327
|
+
function sceneOperationPatchHasLiveConflict(beforeState, changes) {
|
|
1328
|
+
const overrides = useLiveNodeOverrides.getState();
|
|
1329
|
+
const transforms = useLiveTransforms.getState();
|
|
1330
|
+
for (const id of sceneOperationPatchLiveConflictIds(beforeState, changes)) {
|
|
1331
|
+
if (overrides.get(id) || transforms.get(id))
|
|
1332
|
+
return true;
|
|
1333
|
+
}
|
|
1334
|
+
return false;
|
|
1335
|
+
}
|
|
1336
|
+
function areScenePatchValuesEqual(left, right) {
|
|
1337
|
+
if (Object.is(left, right))
|
|
1338
|
+
return true;
|
|
1339
|
+
if (typeof left !== typeof right || left === null || right === null)
|
|
1340
|
+
return false;
|
|
1341
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
1342
|
+
return (Array.isArray(left) &&
|
|
1343
|
+
Array.isArray(right) &&
|
|
1344
|
+
left.length === right.length &&
|
|
1345
|
+
left.every((value, index) => areScenePatchValuesEqual(value, right[index])));
|
|
1346
|
+
}
|
|
1347
|
+
if (typeof left !== 'object' || typeof right !== 'object')
|
|
1348
|
+
return false;
|
|
1349
|
+
const leftRecord = left;
|
|
1350
|
+
const rightRecord = right;
|
|
1351
|
+
const leftKeys = Object.keys(leftRecord);
|
|
1352
|
+
if (leftKeys.length !== Object.keys(rightRecord).length)
|
|
1353
|
+
return false;
|
|
1354
|
+
return leftKeys.every((key) => Object.hasOwn(rightRecord, key) &&
|
|
1355
|
+
areScenePatchValuesEqual(leftRecord[key], rightRecord[key]));
|
|
1356
|
+
}
|
|
1357
|
+
function parseSceneOperationPatchNode(value) {
|
|
1358
|
+
const builtin = AnyNodeSchema.safeParse(value);
|
|
1359
|
+
if (builtin.success)
|
|
1360
|
+
return builtin.data;
|
|
1361
|
+
if (!(value && typeof value === 'object' && !Array.isArray(value)))
|
|
1362
|
+
return null;
|
|
1363
|
+
const type = value.type;
|
|
1364
|
+
if (typeof type !== 'string')
|
|
1365
|
+
return null;
|
|
1366
|
+
const registered = nodeRegistry.get(type)?.schema.safeParse(value);
|
|
1367
|
+
return registered?.success ? registered.data : null;
|
|
1368
|
+
}
|
|
1369
|
+
function structuralSiblingIds(nodes, rootNodeIds, parentId) {
|
|
1370
|
+
if (!parentId)
|
|
1371
|
+
return rootNodeIds;
|
|
1372
|
+
const parent = nodes[parentId];
|
|
1373
|
+
if (!(parent && 'children' in parent && Array.isArray(parent.children)))
|
|
1374
|
+
return null;
|
|
1375
|
+
return parent.children.every((id) => typeof id === 'string')
|
|
1376
|
+
? parent.children
|
|
1377
|
+
: null;
|
|
1378
|
+
}
|
|
1379
|
+
function insertSceneStructuralPlacements(base, placements) {
|
|
1380
|
+
if (placements.length === 0)
|
|
1381
|
+
return base;
|
|
1382
|
+
const result = new Array(base.length + placements.length);
|
|
1383
|
+
for (const change of placements) {
|
|
1384
|
+
if (!Number.isSafeInteger(change.position) ||
|
|
1385
|
+
change.position < 0 ||
|
|
1386
|
+
change.position >= result.length ||
|
|
1387
|
+
result[change.position] !== undefined) {
|
|
1388
|
+
return null;
|
|
1389
|
+
}
|
|
1390
|
+
result[change.position] = change.node.id;
|
|
1391
|
+
}
|
|
1392
|
+
let baseIndex = 0;
|
|
1393
|
+
for (let index = 0; index < result.length; index += 1) {
|
|
1394
|
+
if (result[index] !== undefined)
|
|
1395
|
+
continue;
|
|
1396
|
+
result[index] = base[baseIndex];
|
|
1397
|
+
baseIndex += 1;
|
|
1398
|
+
}
|
|
1399
|
+
return result;
|
|
1400
|
+
}
|
|
1401
|
+
function sceneOperationPatchNextState(beforeState, changes) {
|
|
1402
|
+
const createIds = new Set();
|
|
1403
|
+
const deleteIds = new Set();
|
|
1404
|
+
const updateIds = new Set();
|
|
1405
|
+
const materialIds = new Set();
|
|
1406
|
+
const parsedCreates = [];
|
|
1407
|
+
for (const change of changes.nodeCreates) {
|
|
1408
|
+
const parsed = parseSceneOperationPatchNode(change.node);
|
|
1409
|
+
if (!parsed ||
|
|
1410
|
+
parsed.id !== change.node.id ||
|
|
1411
|
+
createIds.has(parsed.id) ||
|
|
1412
|
+
Object.hasOwn(beforeState.nodes, parsed.id) ||
|
|
1413
|
+
!Number.isSafeInteger(change.position) ||
|
|
1414
|
+
change.position < 0) {
|
|
1415
|
+
return null;
|
|
1416
|
+
}
|
|
1417
|
+
createIds.add(parsed.id);
|
|
1418
|
+
parsedCreates.push({ node: change.node, position: change.position });
|
|
1419
|
+
}
|
|
1420
|
+
for (const change of changes.nodeDeletes) {
|
|
1421
|
+
const id = change.node.id;
|
|
1422
|
+
const current = beforeState.nodes[id];
|
|
1423
|
+
const parentId = change.node.parentId ?? null;
|
|
1424
|
+
const siblings = structuralSiblingIds(beforeState.nodes, beforeState.rootNodeIds, parentId);
|
|
1425
|
+
if (!current ||
|
|
1426
|
+
createIds.has(id) ||
|
|
1427
|
+
deleteIds.has(id) ||
|
|
1428
|
+
!Number.isSafeInteger(change.position) ||
|
|
1429
|
+
change.position < 0 ||
|
|
1430
|
+
siblings?.[change.position] !== id ||
|
|
1431
|
+
!areScenePatchValuesEqual(current, change.node)) {
|
|
1432
|
+
return null;
|
|
1433
|
+
}
|
|
1434
|
+
deleteIds.add(id);
|
|
1435
|
+
}
|
|
1436
|
+
for (const id of createIds) {
|
|
1437
|
+
if (deleteIds.has(id))
|
|
1438
|
+
return null;
|
|
1439
|
+
}
|
|
1440
|
+
for (const node of Object.values(beforeState.nodes)) {
|
|
1441
|
+
const parentId = node.parentId ?? null;
|
|
1442
|
+
if (parentId && deleteIds.has(parentId) && !deleteIds.has(node.id))
|
|
1443
|
+
return null;
|
|
1444
|
+
}
|
|
1445
|
+
const nextNodes = { ...beforeState.nodes };
|
|
1446
|
+
let nextRootNodeIds = deleteIds.size > 0
|
|
1447
|
+
? beforeState.rootNodeIds.filter((id) => !deleteIds.has(id))
|
|
1448
|
+
: beforeState.rootNodeIds;
|
|
1449
|
+
const changedParentIds = new Set();
|
|
1450
|
+
for (const change of changes.nodeDeletes) {
|
|
1451
|
+
const parentId = change.node.parentId ?? null;
|
|
1452
|
+
if (parentId && !deleteIds.has(parentId))
|
|
1453
|
+
changedParentIds.add(parentId);
|
|
1454
|
+
delete nextNodes[change.node.id];
|
|
1455
|
+
}
|
|
1456
|
+
for (const parentId of changedParentIds) {
|
|
1457
|
+
const parent = nextNodes[parentId];
|
|
1458
|
+
if (!(parent && 'children' in parent && Array.isArray(parent.children)))
|
|
1459
|
+
return null;
|
|
1460
|
+
nextNodes[parentId] = {
|
|
1461
|
+
...parent,
|
|
1462
|
+
children: parent.children.filter((id) => !deleteIds.has(id)),
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
for (const change of parsedCreates)
|
|
1466
|
+
nextNodes[change.node.id] = change.node;
|
|
1467
|
+
const rootCreates = [];
|
|
1468
|
+
const existingParentCreates = new Map();
|
|
1469
|
+
for (const change of parsedCreates) {
|
|
1470
|
+
const parentId = change.node.parentId ?? null;
|
|
1471
|
+
if (!parentId) {
|
|
1472
|
+
rootCreates.push(change);
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
const parent = nextNodes[parentId];
|
|
1476
|
+
if (!parent)
|
|
1477
|
+
return null;
|
|
1478
|
+
if (createIds.has(parentId)) {
|
|
1479
|
+
if (!('children' in parent) ||
|
|
1480
|
+
!Array.isArray(parent.children) ||
|
|
1481
|
+
parent.children[change.position] !== change.node.id) {
|
|
1482
|
+
return null;
|
|
1483
|
+
}
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
const placements = existingParentCreates.get(parentId) ?? [];
|
|
1487
|
+
placements.push(change);
|
|
1488
|
+
existingParentCreates.set(parentId, placements);
|
|
1489
|
+
}
|
|
1490
|
+
const insertedRoots = insertSceneStructuralPlacements(nextRootNodeIds, rootCreates);
|
|
1491
|
+
if (!insertedRoots)
|
|
1492
|
+
return null;
|
|
1493
|
+
nextRootNodeIds = insertedRoots;
|
|
1494
|
+
for (const [parentId, placements] of existingParentCreates) {
|
|
1495
|
+
const parent = nextNodes[parentId];
|
|
1496
|
+
if (!(parent && 'children' in parent && Array.isArray(parent.children)))
|
|
1497
|
+
return null;
|
|
1498
|
+
const children = insertSceneStructuralPlacements(parent.children, placements);
|
|
1499
|
+
if (!children)
|
|
1500
|
+
return null;
|
|
1501
|
+
nextNodes[parentId] = { ...parent, children };
|
|
1502
|
+
}
|
|
1503
|
+
for (const change of parsedCreates) {
|
|
1504
|
+
const parentId = change.node.parentId ?? null;
|
|
1505
|
+
const siblings = structuralSiblingIds(nextNodes, nextRootNodeIds, parentId);
|
|
1506
|
+
if (siblings?.[change.position] !== change.node.id)
|
|
1507
|
+
return null;
|
|
1508
|
+
if (!('children' in change.node && Array.isArray(change.node.children)))
|
|
1509
|
+
continue;
|
|
1510
|
+
for (const childId of change.node.children) {
|
|
1511
|
+
if (nextNodes[childId]?.parentId !== change.node.id)
|
|
1512
|
+
return null;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
for (const { id, data, removeFields } of changes.nodeUpdates) {
|
|
1516
|
+
const node = nextNodes[id];
|
|
1517
|
+
if (!node ||
|
|
1518
|
+
createIds.has(id) ||
|
|
1519
|
+
deleteIds.has(id) ||
|
|
1520
|
+
updateIds.has(id) ||
|
|
1521
|
+
('id' in data && data.id !== node.id) ||
|
|
1522
|
+
('type' in data && data.type !== node.type) ||
|
|
1523
|
+
('object' in data && data.object !== node.object) ||
|
|
1524
|
+
removeFields.some((field) => field === 'id' || field === 'object' || field === 'type' || Object.hasOwn(data, field))) {
|
|
1525
|
+
return null;
|
|
1526
|
+
}
|
|
1527
|
+
updateIds.add(id);
|
|
1528
|
+
const candidate = { ...node, ...data };
|
|
1529
|
+
for (const field of removeFields)
|
|
1530
|
+
delete candidate[field];
|
|
1531
|
+
const validated = parseSceneOperationPatchNode(candidate);
|
|
1532
|
+
if (!validated ||
|
|
1533
|
+
validated.id !== id ||
|
|
1534
|
+
validated.type !== node.type ||
|
|
1535
|
+
validated.object !== node.object) {
|
|
1536
|
+
return null;
|
|
1537
|
+
}
|
|
1538
|
+
nextNodes[id] = candidate;
|
|
1539
|
+
}
|
|
1540
|
+
const materials = changes.materialChanges.length > 0 ? { ...beforeState.materials } : beforeState.materials;
|
|
1541
|
+
for (const { id, material } of changes.materialChanges) {
|
|
1542
|
+
if (materialIds.has(id) ||
|
|
1543
|
+
(material !== null && (material.id !== id || !SceneMaterial.safeParse(material).success))) {
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
materialIds.add(id);
|
|
1547
|
+
if (material === null)
|
|
1548
|
+
delete materials[id];
|
|
1549
|
+
else
|
|
1550
|
+
materials[id] = material;
|
|
1551
|
+
}
|
|
1552
|
+
return { materials, nodes: nextNodes, rootNodeIds: nextRootNodeIds };
|
|
1553
|
+
}
|
|
1554
|
+
export function applySceneOperationPatch(changes) {
|
|
1555
|
+
const beforeState = useScene.getState();
|
|
1556
|
+
if (changes.nodeUpdates.length === 0 &&
|
|
1557
|
+
changes.materialChanges.length === 0 &&
|
|
1558
|
+
changes.nodeCreates.length === 0 &&
|
|
1559
|
+
changes.nodeDeletes.length === 0) {
|
|
1560
|
+
return false;
|
|
1561
|
+
}
|
|
1562
|
+
if (sceneOperationPatchHasLiveConflict(beforeState, changes))
|
|
1563
|
+
return false;
|
|
1564
|
+
const next = sceneOperationPatchNextState(beforeState, changes);
|
|
1565
|
+
if (!next)
|
|
1566
|
+
return false;
|
|
1567
|
+
const before = sceneHistorySnapshotFromState(beforeState);
|
|
1568
|
+
const shouldScopeHistoryPause = useScene.temporal.getState().isTracking || getSceneHistoryPauseDepth() > 0;
|
|
1569
|
+
if (shouldScopeHistoryPause)
|
|
1570
|
+
pauseSceneHistory(useScene);
|
|
1571
|
+
try {
|
|
1572
|
+
useScene.setState(next);
|
|
1573
|
+
}
|
|
1574
|
+
finally {
|
|
1575
|
+
if (shouldScopeHistoryPause)
|
|
1576
|
+
resumeSceneHistory(useScene);
|
|
1577
|
+
}
|
|
1578
|
+
const currentState = useScene.getState();
|
|
1579
|
+
const current = sceneHistorySnapshotFromState(currentState);
|
|
1580
|
+
const touchedNodeIds = new Set([
|
|
1581
|
+
...changes.nodeUpdates.map(({ id }) => id),
|
|
1582
|
+
...changes.nodeCreates.map(({ node }) => node.id),
|
|
1583
|
+
...changes.nodeDeletes.map(({ node }) => node.id),
|
|
1584
|
+
]);
|
|
1585
|
+
for (const id of touchedNodeIds) {
|
|
1586
|
+
useLiveNodeOverrides.getState().clear(id);
|
|
1587
|
+
useLiveTransforms.getState().clear(id);
|
|
1588
|
+
}
|
|
1589
|
+
if (areSceneSnapshotsEqual(before, current))
|
|
1590
|
+
return false;
|
|
1591
|
+
for (const id of touchedNodeIds) {
|
|
1592
|
+
if (current.nodes[id])
|
|
1593
|
+
currentState.markDirty(id);
|
|
1594
|
+
else
|
|
1595
|
+
currentState.clearDirty(id);
|
|
1596
|
+
const beforeParentId = before.nodes[id]?.parentId;
|
|
1597
|
+
const currentParentId = current.nodes[id]?.parentId;
|
|
1598
|
+
if (beforeParentId)
|
|
1599
|
+
currentState.markDirty(beforeParentId);
|
|
1600
|
+
if (currentParentId)
|
|
1601
|
+
currentState.markDirty(currentParentId);
|
|
1602
|
+
}
|
|
1603
|
+
const structuralParentIds = new Set();
|
|
1604
|
+
for (const { node } of changes.nodeCreates) {
|
|
1605
|
+
if (node.parentId)
|
|
1606
|
+
structuralParentIds.add(node.parentId);
|
|
1607
|
+
}
|
|
1608
|
+
for (const { node } of changes.nodeDeletes) {
|
|
1609
|
+
if (node.parentId)
|
|
1610
|
+
structuralParentIds.add(node.parentId);
|
|
1611
|
+
}
|
|
1612
|
+
for (const parentId of structuralParentIds) {
|
|
1613
|
+
const parent = current.nodes[parentId];
|
|
1614
|
+
if (!(parent && 'children' in parent && Array.isArray(parent.children)))
|
|
1615
|
+
continue;
|
|
1616
|
+
for (const childId of parent.children)
|
|
1617
|
+
currentState.markDirty(childId);
|
|
1618
|
+
}
|
|
1619
|
+
if (changes.materialChanges.length > 0) {
|
|
1620
|
+
const materialRefs = new Set(changes.materialChanges.map(({ id }) => toSceneMaterialRef(id)));
|
|
1621
|
+
for (const node of Object.values(current.nodes)) {
|
|
1622
|
+
const slots = 'slots' in node ? node.slots : undefined;
|
|
1623
|
+
if (!(slots && Object.values(slots).some((ref) => materialRefs.has(ref))))
|
|
1624
|
+
continue;
|
|
1625
|
+
currentState.markDirty(node.id);
|
|
1626
|
+
if (node.parentId)
|
|
1627
|
+
currentState.markDirty(node.parentId);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
for (const { node } of changes.nodeDeletes)
|
|
1631
|
+
currentState.clearDirty(node.id);
|
|
1632
|
+
notifySceneCommit({
|
|
1633
|
+
origin: 'host',
|
|
1634
|
+
before,
|
|
1635
|
+
current,
|
|
1636
|
+
});
|
|
1637
|
+
return true;
|
|
1638
|
+
}
|
|
1639
|
+
export function applyScenePatch(changes) {
|
|
1640
|
+
return applySceneOperationPatch({
|
|
1641
|
+
...changes,
|
|
1642
|
+
nodeCreates: [],
|
|
1643
|
+
nodeDeletes: [],
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
export function applySceneSnapshot(snapshot, options) {
|
|
1647
|
+
const before = sceneHistorySnapshotFromState(useScene.getState());
|
|
1648
|
+
const temporalState = useScene.temporal.getState();
|
|
1649
|
+
if (!temporalState.isTracking || getSceneHistoryPauseDepth() > 0) {
|
|
1650
|
+
throw new Error('Cannot replace the scene snapshot during an active interaction');
|
|
1651
|
+
}
|
|
1652
|
+
pauseSceneHistory(useScene);
|
|
1653
|
+
try {
|
|
1654
|
+
useScene.getState().setScene(snapshot.nodes, snapshot.rootNodeIds, {
|
|
1655
|
+
collections: snapshot.collections,
|
|
1656
|
+
installedPlugins: snapshot.installedPlugins,
|
|
1657
|
+
materials: snapshot.materials,
|
|
1658
|
+
});
|
|
1659
|
+
useScene.temporal.getState().clear();
|
|
1660
|
+
}
|
|
1661
|
+
finally {
|
|
1662
|
+
resumeSceneHistory(useScene);
|
|
1663
|
+
}
|
|
1664
|
+
useLiveNodeOverrides.getState().clearAll();
|
|
1665
|
+
useLiveTransforms.getState().clearAll();
|
|
1666
|
+
const current = sceneHistorySnapshotFromState(useScene.getState());
|
|
1667
|
+
if (areSceneSnapshotsEqual(before, current))
|
|
1668
|
+
return false;
|
|
1669
|
+
notifySceneCommit({ origin: options.origin, before, current });
|
|
1670
|
+
return true;
|
|
1671
|
+
}
|
|
741
1672
|
// Track previous temporal state lengths and node snapshot for diffing
|
|
742
1673
|
let prevPastLength = 0;
|
|
743
1674
|
let prevFutureLength = 0;
|
|
744
1675
|
let prevNodesSnapshot = null;
|
|
745
1676
|
export function clearSceneHistory() {
|
|
746
1677
|
resetSceneHistoryPauseDepth();
|
|
1678
|
+
// Resetting the pause-depth counter without resuming would strand the
|
|
1679
|
+
// temporal store in `isTracking: false` if a pause window was active when
|
|
1680
|
+
// the scene was (re)loaded — every edit after the load would then be
|
|
1681
|
+
// invisible to undo. Resume unconditionally so the cleared history starts
|
|
1682
|
+
// tracking from the loaded baseline.
|
|
1683
|
+
useScene.temporal.getState().resume();
|
|
747
1684
|
useScene.temporal.getState().clear();
|
|
748
1685
|
prevPastLength = 0;
|
|
749
1686
|
prevFutureLength = 0;
|