@pascal-app/core 0.9.2 → 1.0.0-beta.2
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 +21 -2
- package/dist/events/bus.d.ts.map +1 -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 +116 -66
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +319 -441
- 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 +204 -12
- 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 +23 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -10
- package/dist/lib/measurement-geometry.d.ts +4 -0
- package/dist/lib/measurement-geometry.d.ts.map +1 -1
- package/dist/lib/measurement-geometry.js +27 -10
- package/dist/lib/slab-polygon.d.ts.map +1 -1
- package/dist/lib/slab-polygon.js +33 -1
- package/dist/lib/space-detection.d.ts +15 -4
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +232 -91
- 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 +85 -0
- package/dist/lib/terrain-support.d.ts.map +1 -0
- package/dist/lib/terrain-support.js +218 -0
- package/dist/lib/zone-quantities.d.ts +1 -1
- package/dist/lib/zone-quantities.d.ts.map +1 -1
- package/dist/lib/zone-quantities.js +311 -66
- package/dist/material-library.d.ts +9 -1
- package/dist/material-library.d.ts.map +1 -1
- package/dist/material-library.js +56 -4
- package/dist/registry/handles.d.ts +16 -1
- package/dist/registry/handles.d.ts.map +1 -1
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/subtree.d.ts.map +1 -1
- package/dist/registry/subtree.js +5 -2
- package/dist/registry/types.d.ts +83 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/schema/index.d.ts +6 -3
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +6 -3
- package/dist/schema/nodes/cabinet.d.ts +2 -0
- package/dist/schema/nodes/cabinet.d.ts.map +1 -1
- package/dist/schema/nodes/cabinet.js +4 -2
- package/dist/schema/nodes/ceiling.d.ts +1 -1
- package/dist/schema/nodes/ceiling.d.ts.map +1 -1
- package/dist/schema/nodes/ceiling.js +7 -1
- package/dist/schema/nodes/column.d.ts +1 -0
- package/dist/schema/nodes/column.d.ts.map +1 -1
- package/dist/schema/nodes/column.js +2 -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/door.d.ts +29 -0
- package/dist/schema/nodes/door.d.ts.map +1 -1
- package/dist/schema/nodes/door.js +20 -0
- package/dist/schema/nodes/duct-terminal.d.ts +1 -0
- package/dist/schema/nodes/duct-terminal.d.ts.map +1 -1
- package/dist/schema/nodes/duct-terminal.js +2 -0
- package/dist/schema/nodes/fence.d.ts +2 -0
- package/dist/schema/nodes/fence.d.ts.map +1 -1
- package/dist/schema/nodes/fence.js +8 -0
- package/dist/schema/nodes/hvac-equipment.d.ts +1 -0
- package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -1
- package/dist/schema/nodes/hvac-equipment.js +2 -0
- package/dist/schema/nodes/item.d.ts +1 -0
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +13 -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 -33
- package/dist/schema/nodes/shelf.d.ts +1 -0
- package/dist/schema/nodes/shelf.d.ts.map +1 -1
- package/dist/schema/nodes/shelf.js +2 -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/slab.d.ts +5 -0
- package/dist/schema/nodes/slab.d.ts.map +1 -1
- package/dist/schema/nodes/slab.js +14 -2
- 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.d.ts +3 -1
- package/dist/schema/nodes/stair.d.ts.map +1 -1
- package/dist/schema/nodes/stair.js +8 -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/wall.d.ts +5 -2
- package/dist/schema/nodes/wall.d.ts.map +1 -1
- package/dist/schema/nodes/wall.js +13 -4
- package/dist/schema/nodes/window.d.ts +29 -0
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/nodes/window.js +18 -0
- package/dist/schema/nodes/zone.d.ts +20 -0
- package/dist/schema/nodes/zone.d.ts.map +1 -1
- package/dist/schema/nodes/zone.js +15 -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 +211 -3
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +4 -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/index.d.ts +2 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -1
- package/dist/services/level-height.d.ts +8 -9
- package/dist/services/level-height.d.ts.map +1 -1
- package/dist/services/level-height.js +48 -16
- 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/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +47 -3
- package/dist/store/history-control.d.ts +20 -0
- package/dist/store/history-control.d.ts.map +1 -1
- package/dist/store/history-control.js +110 -13
- 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 +31 -2
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +638 -8
- 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/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 +150 -0
- package/dist/systems/slab/slab-support.d.ts.map +1 -0
- package/dist/systems/slab/slab-support.js +575 -0
- package/dist/systems/stair/stair-opening-preview.d.ts +1060 -935
- 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 +18 -15
- 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.js +6 -6
- 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.map +1 -1
- package/dist/utils/clone-scene-graph.js +34 -3
- package/dist/utils/heal-scene-graph.d.ts +1 -1
- package/dist/utils/heal-scene-graph.d.ts.map +1 -1
- package/dist/utils/heal-scene-graph.js +25 -9
- package/dist/validation/validate-build-json.d.ts +2 -0
- package/dist/validation/validate-build-json.d.ts.map +1 -1
- package/dist/validation/validate-build-json.js +59 -3
- package/package.json +2 -2
- package/dist/hooks/scene-registry/scene-registry.test.d.ts +0 -2
- package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +0 -1
- package/dist/hooks/scene-registry/scene-registry.test.js +0 -26
- 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 -366
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +0 -2
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +0 -1
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +0 -363
- package/dist/lib/measurement-geometry.test.d.ts +0 -2
- package/dist/lib/measurement-geometry.test.d.ts.map +0 -1
- package/dist/lib/measurement-geometry.test.js +0 -102
- package/dist/lib/slab-polygon.test.d.ts +0 -2
- package/dist/lib/slab-polygon.test.d.ts.map +0 -1
- package/dist/lib/slab-polygon.test.js +0 -635
- 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 -376
- package/dist/lib/zone-quantities.test.d.ts +0 -2
- package/dist/lib/zone-quantities.test.d.ts.map +0 -1
- package/dist/lib/zone-quantities.test.js +0 -82
- 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 -196
- 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 -163
- 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/schema/material.test.d.ts +0 -2
- package/dist/schema/material.test.d.ts.map +0 -1
- package/dist/schema/material.test.js +0 -43
- package/dist/schema/nodes/measurement.test.d.ts +0 -2
- package/dist/schema/nodes/measurement.test.d.ts.map +0 -1
- package/dist/schema/nodes/measurement.test.js +0 -139
- package/dist/schema/nodes/ridge-vent-defaults.test.d.ts +0 -2
- package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +0 -1
- package/dist/schema/nodes/ridge-vent-defaults.test.js +0 -205
- package/dist/schema/nodes/roof-segment-shape.test.d.ts +0 -2
- package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +0 -1
- package/dist/schema/nodes/roof-segment-shape.test.js +0 -175
- package/dist/schema/nodes/roof-segment-surface.test.d.ts +0 -2
- package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +0 -1
- package/dist/schema/nodes/roof-segment-surface.test.js +0 -48
- package/dist/schema/nodes/roof-segment-trim.test.d.ts +0 -2
- package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +0 -1
- package/dist/schema/nodes/roof-segment-trim.test.js +0 -107
- package/dist/schema/nodes/roof-segment-walls.test.d.ts +0 -2
- package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +0 -1
- package/dist/schema/nodes/roof-segment-walls.test.js +0 -92
- package/dist/schema/nodes/wall.test.d.ts +0 -2
- package/dist/schema/nodes/wall.test.d.ts.map +0 -1
- package/dist/schema/nodes/wall.test.js +0 -208
- 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 -230
- 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/opening-guides.test.d.ts +0 -2
- package/dist/services/opening-guides.test.d.ts.map +0 -1
- package/dist/services/opening-guides.test.js +0 -208
- package/dist/services/port-connectivity.test.d.ts +0 -2
- package/dist/services/port-connectivity.test.d.ts.map +0 -1
- package/dist/services/port-connectivity.test.js +0 -297
- package/dist/services/riser-diagram.test.d.ts +0 -2
- package/dist/services/riser-diagram.test.d.ts.map +0 -1
- package/dist/services/riser-diagram.test.js +0 -66
- 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 -137
- package/dist/services/system-graph.test.d.ts +0 -2
- package/dist/services/system-graph.test.d.ts.map +0 -1
- package/dist/services/system-graph.test.js +0 -138
- package/dist/services/validate-dwv.test.d.ts +0 -2
- package/dist/services/validate-dwv.test.d.ts.map +0 -1
- package/dist/services/validate-dwv.test.js +0 -109
- 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/actions/ridge-vent-update.test.d.ts +0 -2
- package/dist/store/actions/ridge-vent-update.test.d.ts.map +0 -1
- package/dist/store/actions/ridge-vent-update.test.js +0 -264
- package/dist/store/use-scene-dirty-tracking.test.d.ts +0 -2
- package/dist/store/use-scene-dirty-tracking.test.d.ts.map +0 -1
- package/dist/store/use-scene-dirty-tracking.test.js +0 -68
- 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/store/use-scene-measurement-roundtrip.test.d.ts +0 -2
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +0 -1
- package/dist/store/use-scene-measurement-roundtrip.test.js +0 -97
- package/dist/store/use-scene-plugins.test.d.ts +0 -2
- package/dist/store/use-scene-plugins.test.d.ts.map +0 -1
- package/dist/store/use-scene-plugins.test.js +0 -51
- package/dist/store/use-scene-wall-slot-migration.test.d.ts +0 -2
- package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +0 -1
- package/dist/store/use-scene-wall-slot-migration.test.js +0 -210
- package/dist/store/use-scene-window-migration.test.d.ts +0 -2
- package/dist/store/use-scene-window-migration.test.d.ts.map +0 -1
- package/dist/store/use-scene-window-migration.test.js +0 -85
- 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/fence/fence-spline.test.d.ts +0 -2
- package/dist/systems/fence/fence-spline.test.d.ts.map +0 -1
- package/dist/systems/fence/fence-spline.test.js +0 -136
- 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 -485
- package/dist/systems/wall/wall-mitering.test.d.ts +0 -2
- package/dist/systems/wall/wall-mitering.test.d.ts.map +0 -1
- package/dist/systems/wall/wall-mitering.test.js +0 -63
- 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 -62
- package/dist/utils/heal-scene-graph.test.d.ts +0 -2
- package/dist/utils/heal-scene-graph.test.d.ts.map +0 -1
- package/dist/utils/heal-scene-graph.test.js +0 -88
package/dist/store/use-scene.js
CHANGED
|
@@ -16,10 +16,17 @@ import { getEffectiveStairSurfaceMaterial, StairNode as StairNodeSchema, } from
|
|
|
16
16
|
import { StairSegmentNode as StairSegmentNodeSchema } from '../schema/nodes/stair-segment';
|
|
17
17
|
import { getEffectiveWallSurfaceMaterial } from '../schema/nodes/wall';
|
|
18
18
|
import { WindowNode as WindowNodeSchema } from '../schema/nodes/window';
|
|
19
|
-
import { generateSceneMaterialId, } from '../schema/scene-material';
|
|
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';
|
|
20
25
|
import { healSceneNodes } from '../utils/heal-scene-graph';
|
|
21
26
|
import * as nodeActions from './actions/node-actions';
|
|
22
|
-
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';
|
|
23
30
|
function getFiniteNumber(value, fallback) {
|
|
24
31
|
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
25
32
|
}
|
|
@@ -52,6 +59,7 @@ function getVector3(value, fallback) {
|
|
|
52
59
|
];
|
|
53
60
|
}
|
|
54
61
|
function normalizeStairNode(node) {
|
|
62
|
+
const hasTotalRise = 'totalRise' in node;
|
|
55
63
|
const sanitized = {
|
|
56
64
|
...node,
|
|
57
65
|
position: getVector3(node.position, [0, 0, 0]),
|
|
@@ -62,7 +70,7 @@ function normalizeStairNode(node) {
|
|
|
62
70
|
slabOpeningMode: getEnumValue(node.slabOpeningMode, ['none', 'destination'], 'none'),
|
|
63
71
|
openingOffset: getFiniteNumber(node.openingOffset, 0),
|
|
64
72
|
width: getFiniteNumber(node.width, 1),
|
|
65
|
-
totalRise: getFiniteNumber(node.totalRise, 2.5),
|
|
73
|
+
totalRise: hasTotalRise ? getFiniteNumber(node.totalRise, 2.5) : undefined,
|
|
66
74
|
stepCount: getFiniteNumber(node.stepCount, 10),
|
|
67
75
|
thickness: getFiniteNumber(node.thickness, 0.25),
|
|
68
76
|
fillToFloor: getBoolean(node.fillToFloor, true),
|
|
@@ -77,7 +85,15 @@ function normalizeStairNode(node) {
|
|
|
77
85
|
children: getStringArray(node.children),
|
|
78
86
|
};
|
|
79
87
|
const parsed = StairNodeSchema.safeParse(sanitized);
|
|
80
|
-
|
|
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;
|
|
81
97
|
}
|
|
82
98
|
function normalizeStairSegmentNode(node) {
|
|
83
99
|
const sanitized = {
|
|
@@ -434,11 +450,74 @@ function migrateRoofSurfaceMaterials(node) {
|
|
|
434
450
|
}
|
|
435
451
|
return next;
|
|
436
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;
|
|
437
515
|
function migrateNodes(nodes) {
|
|
438
516
|
// Repair pre-existing corruption (null children, zero-length walls) before
|
|
439
517
|
// any per-type migration runs, so already-saved scenes load cleanly.
|
|
440
518
|
const { nodes: healed } = healSceneNodes(nodes);
|
|
441
519
|
const patchedNodes = { ...healed };
|
|
520
|
+
removeRetiredDrawingSheets(patchedNodes);
|
|
442
521
|
// Scene materials minted while moving legacy wall fields onto `node.slots`;
|
|
443
522
|
// merged into the scene material map by the caller (`setScene`).
|
|
444
523
|
const mintedMaterials = {};
|
|
@@ -530,6 +609,9 @@ function migrateNodes(nodes) {
|
|
|
530
609
|
patchedNodes[id] = normalized;
|
|
531
610
|
}
|
|
532
611
|
}
|
|
612
|
+
if (node.type === 'construction-dimension') {
|
|
613
|
+
patchedNodes[id] = migrateConstructionDimension(node);
|
|
614
|
+
}
|
|
533
615
|
if (node.type === 'stair') {
|
|
534
616
|
const normalized = normalizeStairNode(migrateStairSurfaceMaterials(node));
|
|
535
617
|
if (normalized) {
|
|
@@ -544,7 +626,7 @@ function migrateNodes(nodes) {
|
|
|
544
626
|
}
|
|
545
627
|
}
|
|
546
628
|
if (node.type === 'wall') {
|
|
547
|
-
patchedNodes[id] = migrateWallSurfaceMaterials(patchedNodes[id], mintedMaterials);
|
|
629
|
+
patchedNodes[id] = migrateWallSurfaceMaterials(migrateWallAssembly(patchedNodes[id]), mintedMaterials);
|
|
548
630
|
}
|
|
549
631
|
// Cabinet v2→v3: node-level `doorStyle` was dead (geometry reads only the
|
|
550
632
|
// per-compartment `doorType`) and was removed from both cabinet schemas;
|
|
@@ -709,6 +791,154 @@ function migrateNodes(nodes) {
|
|
|
709
791
|
patchedNodes[id] = normalized;
|
|
710
792
|
}
|
|
711
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
|
+
}
|
|
941
|
+
}
|
|
712
942
|
return { nodes: patchedNodes, mintedMaterials };
|
|
713
943
|
}
|
|
714
944
|
function getNodeChildIds(node) {
|
|
@@ -759,6 +989,10 @@ function collectReachableNodeIds(nodes, rootNodeIds) {
|
|
|
759
989
|
}
|
|
760
990
|
return reachable;
|
|
761
991
|
}
|
|
992
|
+
function sceneHistorySnapshotFromState(state) {
|
|
993
|
+
const { nodes, rootNodeIds, collections, materials, installedPlugins } = state;
|
|
994
|
+
return { nodes, rootNodeIds, collections, materials, installedPlugins };
|
|
995
|
+
}
|
|
762
996
|
const useScene = create()(temporal((set, get) => ({
|
|
763
997
|
// 1. Flat dictionary of all nodes
|
|
764
998
|
nodes: {},
|
|
@@ -870,6 +1104,7 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
870
1104
|
const level0 = LevelNode.parse({
|
|
871
1105
|
level: 0,
|
|
872
1106
|
children: [],
|
|
1107
|
+
height: 2.5,
|
|
873
1108
|
});
|
|
874
1109
|
const building = BuildingNode.parse({
|
|
875
1110
|
children: [level0.id],
|
|
@@ -1032,13 +1267,408 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
1032
1267
|
});
|
|
1033
1268
|
},
|
|
1034
1269
|
}), {
|
|
1035
|
-
partialize: (state) =>
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
+
});
|
|
1038
1278
|
},
|
|
1039
1279
|
limit: 50, // Limit to last 50 actions
|
|
1040
1280
|
}));
|
|
1041
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
|
+
}
|
|
1042
1672
|
// Track previous temporal state lengths and node snapshot for diffing
|
|
1043
1673
|
let prevPastLength = 0;
|
|
1044
1674
|
let prevFutureLength = 0;
|