@pascal-app/core 0.9.1 → 0.9.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/dist/events/bus.d.ts +30 -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/scene-registry/scene-registry.test.d.ts +2 -0
- package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +1 -0
- package/dist/hooks/scene-registry/scene-registry.test.js +26 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +93 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +69 -12
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +476 -195
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +19 -1
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +2 -0
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +363 -0
- package/dist/index.d.ts +16 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -5
- 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 +19 -0
- package/dist/lib/measurement-geometry.d.ts.map +1 -0
- package/dist/lib/measurement-geometry.js +232 -0
- package/dist/lib/measurement-geometry.test.d.ts +2 -0
- package/dist/lib/measurement-geometry.test.d.ts.map +1 -0
- package/dist/lib/measurement-geometry.test.js +102 -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 +525 -31
- package/dist/lib/slab-polygon.test.d.ts +2 -0
- package/dist/lib/slab-polygon.test.d.ts.map +1 -0
- package/dist/lib/slab-polygon.test.js +635 -0
- 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 +25 -2
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +414 -118
- package/dist/lib/space-detection.test.js +301 -2
- 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 +183 -0
- package/dist/lib/zone-quantities.test.d.ts +2 -0
- package/dist/lib/zone-quantities.test.d.ts.map +1 -0
- package/dist/lib/zone-quantities.test.js +82 -0
- package/dist/material-library.d.ts +20 -1
- package/dist/material-library.d.ts.map +1 -1
- package/dist/material-library.js +2181 -441
- package/dist/registry/handles.d.ts +75 -2
- 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/registry.test.js +14 -1
- package/dist/registry/subtree.d.ts.map +1 -1
- package/dist/registry/subtree.js +4 -0
- package/dist/registry/subtree.test.js +30 -0
- package/dist/registry/types.d.ts +717 -38
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/schema/index.d.ts +21 -5
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +17 -3
- 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/material.test.d.ts +2 -0
- package/dist/schema/material.test.d.ts.map +1 -0
- package/dist/schema/material.test.js +43 -0
- package/dist/schema/nodes/box-vent.d.ts +2 -2
- package/dist/schema/nodes/cabinet.d.ts +534 -0
- package/dist/schema/nodes/cabinet.d.ts.map +1 -0
- package/dist/schema/nodes/cabinet.js +137 -0
- package/dist/schema/nodes/ceiling.d.ts +3 -2
- package/dist/schema/nodes/ceiling.d.ts.map +1 -1
- package/dist/schema/nodes/ceiling.js +4 -0
- package/dist/schema/nodes/chimney.d.ts +4 -4
- package/dist/schema/nodes/column.d.ts +3 -2
- package/dist/schema/nodes/column.d.ts.map +1 -1
- package/dist/schema/nodes/column.js +3 -0
- package/dist/schema/nodes/cupola.d.ts +2 -2
- package/dist/schema/nodes/door.d.ts +13 -5
- package/dist/schema/nodes/door.d.ts.map +1 -1
- package/dist/schema/nodes/door.js +12 -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 +57 -0
- package/dist/schema/nodes/duct-terminal.d.ts.map +1 -0
- package/dist/schema/nodes/duct-terminal.js +51 -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 +18 -2
- package/dist/schema/nodes/fence.d.ts.map +1 -1
- package/dist/schema/nodes/fence.js +25 -2
- package/dist/schema/nodes/gutter.d.ts +2 -2
- package/dist/schema/nodes/hvac-equipment.d.ts +63 -0
- package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -0
- package/dist/schema/nodes/hvac-equipment.js +55 -0
- package/dist/schema/nodes/item.d.ts +8 -0
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +11 -0
- package/dist/schema/nodes/level.d.ts +1 -1
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +2 -0
- 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/measurement.test.d.ts +2 -0
- package/dist/schema/nodes/measurement.test.d.ts.map +1 -0
- package/dist/schema/nodes/measurement.test.js +139 -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-defaults.test.d.ts +2 -0
- package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +1 -0
- package/dist/schema/nodes/ridge-vent-defaults.test.js +205 -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-shape.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-shape.test.js +175 -0
- package/dist/schema/nodes/roof-segment-surface.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-surface.test.js +48 -0
- package/dist/schema/nodes/roof-segment-trim.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-trim.test.js +107 -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-walls.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-walls.test.js +92 -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 +3 -2
- package/dist/schema/nodes/shelf.d.ts.map +1 -1
- package/dist/schema/nodes/shelf.js +6 -0
- package/dist/schema/nodes/skylight.d.ts +10 -10
- package/dist/schema/nodes/slab.d.ts +3 -2
- package/dist/schema/nodes/slab.d.ts.map +1 -1
- package/dist/schema/nodes/slab.js +4 -0
- package/dist/schema/nodes/solar-panel.d.ts +4 -4
- package/dist/schema/nodes/stair-segment.d.ts +4 -4
- package/dist/schema/nodes/stair.d.ts +13 -12
- package/dist/schema/nodes/stair.d.ts.map +1 -1
- package/dist/schema/nodes/stair.js +3 -0
- package/dist/schema/nodes/turbine-vent.d.ts +2 -2
- package/dist/schema/nodes/wall.d.ts +245 -6
- package/dist/schema/nodes/wall.d.ts.map +1 -1
- package/dist/schema/nodes/wall.js +257 -0
- package/dist/schema/nodes/wall.test.d.ts +2 -0
- package/dist/schema/nodes/wall.test.d.ts.map +1 -0
- package/dist/schema/nodes/wall.test.js +208 -0
- package/dist/schema/nodes/window.d.ts +11 -3
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/nodes/window.js +12 -0
- package/dist/schema/nodes/zone.d.ts +2 -0
- package/dist/schema/nodes/zone.d.ts.map +1 -1
- package/dist/schema/nodes/zone.js +6 -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/types.d.ts +1089 -94
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +23 -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/hosting.test.js +32 -1
- package/dist/services/index.d.ts +8 -2
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +8 -2
- package/dist/services/level-height.d.ts +28 -0
- package/dist/services/level-height.d.ts.map +1 -0
- package/dist/services/level-height.js +62 -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/opening-guides.test.d.ts +2 -0
- package/dist/services/opening-guides.test.d.ts.map +1 -0
- package/dist/services/opening-guides.test.js +208 -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/port-connectivity.test.d.ts +2 -0
- package/dist/services/port-connectivity.test.d.ts.map +1 -0
- package/dist/services/port-connectivity.test.js +297 -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/riser-diagram.test.d.ts +2 -0
- package/dist/services/riser-diagram.test.d.ts.map +1 -0
- package/dist/services/riser-diagram.test.js +66 -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/snap.test.js +43 -1
- 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/system-graph.test.d.ts +2 -0
- package/dist/services/system-graph.test.d.ts.map +1 -0
- package/dist/services/system-graph.test.js +138 -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/services/validate-dwv.test.d.ts +2 -0
- package/dist/services/validate-dwv.test.d.ts.map +1 -0
- package/dist/services/validate-dwv.test.js +109 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +94 -1
- package/dist/store/actions/ridge-vent-update.test.d.ts +2 -0
- package/dist/store/actions/ridge-vent-update.test.d.ts.map +1 -0
- package/dist/store/actions/ridge-vent-update.test.js +264 -0
- package/dist/store/history-control.d.ts +11 -0
- package/dist/store/history-control.d.ts.map +1 -1
- package/dist/store/history-control.js +33 -0
- package/dist/store/use-scene-dirty-tracking.test.d.ts +2 -0
- package/dist/store/use-scene-dirty-tracking.test.d.ts.map +1 -0
- package/dist/store/use-scene-dirty-tracking.test.js +68 -0
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts +2 -0
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +1 -0
- package/dist/store/use-scene-measurement-roundtrip.test.js +97 -0
- package/dist/store/use-scene-plugins.test.d.ts +2 -0
- package/dist/store/use-scene-plugins.test.d.ts.map +1 -0
- package/dist/store/use-scene-plugins.test.js +51 -0
- package/dist/store/use-scene-wall-slot-migration.test.d.ts +2 -0
- package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +1 -0
- package/dist/store/use-scene-wall-slot-migration.test.js +210 -0
- package/dist/store/use-scene-window-migration.test.d.ts +2 -0
- package/dist/store/use-scene-window-migration.test.d.ts.map +1 -0
- package/dist/store/use-scene-window-migration.test.js +85 -0
- package/dist/store/use-scene.d.ts +17 -2
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +358 -51
- package/dist/systems/elevator/elevator-service.js +1 -1
- 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/fence/fence-spline.test.d.ts +2 -0
- package/dist/systems/fence/fence-spline.test.d.ts.map +1 -0
- package/dist/systems/fence/fence-spline.test.js +136 -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 +1139 -469
- 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 +1 -18
- package/dist/systems/stair/stair-opening-sync.test.js +39 -0
- package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
- package/dist/systems/wall/wall-mitering.js +38 -2
- package/dist/systems/wall/wall-mitering.test.d.ts +2 -0
- package/dist/systems/wall/wall-mitering.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-mitering.test.js +63 -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 +23 -3
- package/dist/utils/clone-scene-graph.test.js +3 -0
- 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 +90 -0
- package/dist/utils/heal-scene-graph.test.d.ts +2 -0
- package/dist/utils/heal-scene-graph.test.d.ts.map +1 -0
- package/dist/utils/heal-scene-graph.test.js +88 -0
- package/dist/validation/validate-build-json.d.ts +1 -0
- package/dist/validation/validate-build-json.d.ts.map +1 -1
- package/dist/validation/validate-build-json.js +31 -2
- package/package.json +3 -3
package/dist/store/use-scene.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
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, } from '../schema/scene-material';
|
|
20
|
+
import { healSceneNodes } from '../utils/heal-scene-graph';
|
|
14
21
|
import * as nodeActions from './actions/node-actions';
|
|
15
22
|
import { resetSceneHistoryPauseDepth } from './history-control';
|
|
16
23
|
function getFiniteNumber(value, fallback) {
|
|
@@ -93,6 +100,13 @@ function normalizeDoorNode(node) {
|
|
|
93
100
|
const parsed = DoorNodeSchema.safeParse(node);
|
|
94
101
|
return parsed.success ? { ...node, ...parsed.data } : null;
|
|
95
102
|
}
|
|
103
|
+
// Windows saved before a schema field existed (e.g. `columnRatios`/`rowRatios`/
|
|
104
|
+
// `frameThickness`) load without it; the mesh builder then reads undefined and
|
|
105
|
+
// throws every frame. Zod-parse on load so schema defaults land, like doors.
|
|
106
|
+
function normalizeWindowNode(node) {
|
|
107
|
+
const parsed = WindowNodeSchema.safeParse(node);
|
|
108
|
+
return parsed.success ? { ...node, ...parsed.data } : null;
|
|
109
|
+
}
|
|
96
110
|
function normalizeShelfNode(node) {
|
|
97
111
|
const sanitized = {
|
|
98
112
|
...node,
|
|
@@ -182,40 +196,127 @@ function migrateElevatorParent(id, node, nodes) {
|
|
|
182
196
|
parentId: buildingId,
|
|
183
197
|
};
|
|
184
198
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) {
|
|
194
|
-
return node;
|
|
199
|
+
// Reuse an already-minted scene material for an identical inline legacy
|
|
200
|
+
// material so a whole building painted one custom colour collapses to one
|
|
201
|
+
// shared datablock (mirrors `commitSlotPaint`'s dedupe-on-match).
|
|
202
|
+
function findMintedSceneMaterialRef(material, mintedMaterials) {
|
|
203
|
+
const target = JSON.stringify(material);
|
|
204
|
+
for (const sceneMaterial of Object.values(mintedMaterials)) {
|
|
205
|
+
if (JSON.stringify(sceneMaterial.material) === target) {
|
|
206
|
+
return toSceneMaterialRef(sceneMaterial.id);
|
|
195
207
|
}
|
|
196
|
-
return {
|
|
197
|
-
...node,
|
|
198
|
-
interiorMaterial: legacyFinish.material,
|
|
199
|
-
interiorMaterialPreset: legacyFinish.materialPreset,
|
|
200
|
-
exteriorMaterial: legacyFinish.material,
|
|
201
|
-
exteriorMaterialPreset: legacyFinish.materialPreset,
|
|
202
|
-
};
|
|
203
208
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
// Turn a legacy surface spec (`{ material, materialPreset }`) into a
|
|
212
|
+
// `MaterialRef`: a preset that's already a `library:`/`scene:` ref is used
|
|
213
|
+
// as-is; an inline material mints (or reuses) a scene material. Returns
|
|
214
|
+
// undefined when the spec carries no material. Shared by every legacy→slots
|
|
215
|
+
// migration below.
|
|
216
|
+
function legacySpecToMaterialRef(spec, mintedMaterials) {
|
|
217
|
+
if (typeof spec.materialPreset === 'string' && parseMaterialRef(spec.materialPreset)) {
|
|
218
|
+
return spec.materialPreset;
|
|
210
219
|
}
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
220
|
+
if (spec.material !== undefined) {
|
|
221
|
+
const existing = findMintedSceneMaterialRef(spec.material, mintedMaterials);
|
|
222
|
+
if (existing)
|
|
223
|
+
return existing;
|
|
224
|
+
const id = generateSceneMaterialId();
|
|
225
|
+
mintedMaterials[id] = {
|
|
226
|
+
id,
|
|
227
|
+
name: `Material ${Object.keys(mintedMaterials).length + 1}`,
|
|
228
|
+
material: spec.material,
|
|
216
229
|
};
|
|
230
|
+
return toSceneMaterialRef(id);
|
|
217
231
|
}
|
|
218
|
-
return
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
// Move the retired inline `material*` / `interiorMaterial*` / `exteriorMaterial*`
|
|
235
|
+
// fields onto the unified `node.slots` model (interior / exterior → a
|
|
236
|
+
// `library:`/`scene:` ref), minting scene materials for inline customs into
|
|
237
|
+
// `mintedMaterials` (merged into the scene material map by the caller). Already
|
|
238
|
+
// slot-modelled walls and walls with no legacy material are left untouched.
|
|
239
|
+
function migrateWallSurfaceMaterials(node, mintedMaterials) {
|
|
240
|
+
if (node.slots && (node.slots.interior !== undefined || node.slots.exterior !== undefined)) {
|
|
241
|
+
return node;
|
|
242
|
+
}
|
|
243
|
+
const slots = { ...(node.slots ?? {}) };
|
|
244
|
+
for (const side of ['interior', 'exterior']) {
|
|
245
|
+
const spec = getEffectiveWallSurfaceMaterial(node, side);
|
|
246
|
+
const ref = legacySpecToMaterialRef(spec, mintedMaterials);
|
|
247
|
+
if (ref)
|
|
248
|
+
slots[side] = ref;
|
|
249
|
+
}
|
|
250
|
+
if (Object.keys(slots).length === 0) {
|
|
251
|
+
return node;
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
...node,
|
|
255
|
+
slots,
|
|
256
|
+
material: undefined,
|
|
257
|
+
materialPreset: undefined,
|
|
258
|
+
interiorMaterial: undefined,
|
|
259
|
+
interiorMaterialPreset: undefined,
|
|
260
|
+
exteriorMaterial: undefined,
|
|
261
|
+
exteriorMaterialPreset: undefined,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
// Move a kind's single legacy `material` / `materialPreset` onto its declared
|
|
265
|
+
// slots. A pre-slot-model node painted one material rendered that material on
|
|
266
|
+
// every part (each slot resolves `node.slots[slot]` → legacy → default), so the
|
|
267
|
+
// migration writes the same ref to every slot id the kind can expose — unused
|
|
268
|
+
// conditional slots are harmless. Already slot-modelled or unpainted nodes are
|
|
269
|
+
// left untouched. Mirrors `migrateWallSurfaceMaterials` for single-surface and
|
|
270
|
+
// whole-object kinds (slab, ceiling, fence, column, shelf).
|
|
271
|
+
function migrateSingleMaterialSlots(node, slotIds, mintedMaterials) {
|
|
272
|
+
if (node.slots && Object.keys(node.slots).length > 0) {
|
|
273
|
+
return node;
|
|
274
|
+
}
|
|
275
|
+
const ref = legacySpecToMaterialRef({ material: node.material, materialPreset: node.materialPreset }, mintedMaterials);
|
|
276
|
+
if (!ref) {
|
|
277
|
+
return node;
|
|
278
|
+
}
|
|
279
|
+
const slots = {};
|
|
280
|
+
for (const slotId of slotIds)
|
|
281
|
+
slots[slotId] = ref;
|
|
282
|
+
return { ...node, slots, material: undefined, materialPreset: undefined };
|
|
283
|
+
}
|
|
284
|
+
// Stair carries per-role legacy fields (`treadMaterial*` / `sideMaterial*` /
|
|
285
|
+
// `railingMaterial*`) plus a catch-all. Map each to its slot via the same
|
|
286
|
+
// fallback chain the renderer uses (`getEffectiveStairSurfaceMaterial`):
|
|
287
|
+
// tread→treads, side→body, railing→railing. Runs after
|
|
288
|
+
// `migrateStairSurfaceMaterials` has normalised the legacy fields.
|
|
289
|
+
function migrateStairSurfaceSlots(node, mintedMaterials) {
|
|
290
|
+
if (node.slots && Object.keys(node.slots).length > 0) {
|
|
291
|
+
return node;
|
|
292
|
+
}
|
|
293
|
+
const roleToSlot = [
|
|
294
|
+
['tread', 'treads'],
|
|
295
|
+
['side', 'body'],
|
|
296
|
+
['railing', 'railing'],
|
|
297
|
+
];
|
|
298
|
+
const slots = {};
|
|
299
|
+
for (const [role, slotId] of roleToSlot) {
|
|
300
|
+
const spec = getEffectiveStairSurfaceMaterial(node, role);
|
|
301
|
+
const ref = legacySpecToMaterialRef(spec, mintedMaterials);
|
|
302
|
+
if (ref)
|
|
303
|
+
slots[slotId] = ref;
|
|
304
|
+
}
|
|
305
|
+
if (Object.keys(slots).length === 0) {
|
|
306
|
+
return node;
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
...node,
|
|
310
|
+
slots,
|
|
311
|
+
material: undefined,
|
|
312
|
+
materialPreset: undefined,
|
|
313
|
+
treadMaterial: undefined,
|
|
314
|
+
treadMaterialPreset: undefined,
|
|
315
|
+
sideMaterial: undefined,
|
|
316
|
+
sideMaterialPreset: undefined,
|
|
317
|
+
railingMaterial: undefined,
|
|
318
|
+
railingMaterialPreset: undefined,
|
|
319
|
+
};
|
|
219
320
|
}
|
|
220
321
|
function migrateStairSurfaceMaterials(node) {
|
|
221
322
|
const hasRailing = node.railingMaterial !== undefined || typeof node.railingMaterialPreset === 'string';
|
|
@@ -334,7 +435,20 @@ function migrateRoofSurfaceMaterials(node) {
|
|
|
334
435
|
return next;
|
|
335
436
|
}
|
|
336
437
|
function migrateNodes(nodes) {
|
|
337
|
-
|
|
438
|
+
// Repair pre-existing corruption (null children, zero-length walls) before
|
|
439
|
+
// any per-type migration runs, so already-saved scenes load cleanly.
|
|
440
|
+
const { nodes: healed } = healSceneNodes(nodes);
|
|
441
|
+
const patchedNodes = { ...healed };
|
|
442
|
+
// Scene materials minted while moving legacy wall fields onto `node.slots`;
|
|
443
|
+
// merged into the scene material map by the caller (`setScene`).
|
|
444
|
+
const mintedMaterials = {};
|
|
445
|
+
// Pass 1: all node types except elevator.
|
|
446
|
+
// Elevator migration (migrateElevatorParent) mutates level.children to remove
|
|
447
|
+
// the elevator ID. If the elevator is processed before its parent level in
|
|
448
|
+
// Object.entries order, the level migration in this same pass would then see
|
|
449
|
+
// a children array that still contains the elevator ID and filter it out as
|
|
450
|
+
// "missing" — corrupting the level. Running elevators in a second pass after
|
|
451
|
+
// all levels are stable avoids the race entirely.
|
|
338
452
|
for (const [id, node] of Object.entries(patchedNodes)) {
|
|
339
453
|
// 1. Item scale migration
|
|
340
454
|
if (node.type === 'item' && !('scale' in node)) {
|
|
@@ -410,11 +524,18 @@ function migrateNodes(nodes) {
|
|
|
410
524
|
patchedNodes[id] = normalized;
|
|
411
525
|
}
|
|
412
526
|
}
|
|
527
|
+
if (node.type === 'window') {
|
|
528
|
+
const normalized = normalizeWindowNode(node);
|
|
529
|
+
if (normalized) {
|
|
530
|
+
patchedNodes[id] = normalized;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
413
533
|
if (node.type === 'stair') {
|
|
414
534
|
const normalized = normalizeStairNode(migrateStairSurfaceMaterials(node));
|
|
415
535
|
if (normalized) {
|
|
416
536
|
patchedNodes[id] = normalized;
|
|
417
537
|
}
|
|
538
|
+
patchedNodes[id] = migrateStairSurfaceSlots(patchedNodes[id], mintedMaterials);
|
|
418
539
|
}
|
|
419
540
|
if (node.type === 'stair-segment') {
|
|
420
541
|
const normalized = normalizeStairSegmentNode(node);
|
|
@@ -423,20 +544,52 @@ function migrateNodes(nodes) {
|
|
|
423
544
|
}
|
|
424
545
|
}
|
|
425
546
|
if (node.type === 'wall') {
|
|
426
|
-
patchedNodes[id] = migrateWallSurfaceMaterials(patchedNodes[id]);
|
|
547
|
+
patchedNodes[id] = migrateWallSurfaceMaterials(patchedNodes[id], mintedMaterials);
|
|
427
548
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
549
|
+
// Cabinet v2→v3: node-level `doorStyle` was dead (geometry reads only the
|
|
550
|
+
// per-compartment `doorType`) and was removed from both cabinet schemas;
|
|
551
|
+
// `handlePosition: 'edge'` behaved identically to 'auto' and was dropped
|
|
552
|
+
// from the enum; the compartment stack became a discriminated union that
|
|
553
|
+
// rejects a `cooktopLayout` mismatched to its gas/induction type (the old
|
|
554
|
+
// loose schema ignored it).
|
|
555
|
+
if (node.type === 'cabinet' || node.type === 'cabinet-module') {
|
|
556
|
+
const { doorStyle: _doorStyle, ...rest } = node;
|
|
557
|
+
const next = rest;
|
|
558
|
+
if (next.handlePosition === 'edge')
|
|
559
|
+
next.handlePosition = 'auto';
|
|
560
|
+
if (Array.isArray(next.stack)) {
|
|
561
|
+
next.stack = next.stack.map((compartment) => {
|
|
562
|
+
if (!compartment || typeof compartment !== 'object')
|
|
563
|
+
return compartment;
|
|
564
|
+
const layout = compartment.cooktopLayout;
|
|
565
|
+
if (typeof layout !== 'string')
|
|
566
|
+
return compartment;
|
|
567
|
+
if (compartment.type === 'cooktop-gas' && !layout.startsWith('gas-')) {
|
|
568
|
+
return { ...compartment, cooktopLayout: 'gas-4burner' };
|
|
569
|
+
}
|
|
570
|
+
if (compartment.type === 'cooktop-induction' && !layout.startsWith('induction-')) {
|
|
571
|
+
return { ...compartment, cooktopLayout: 'induction-4zone' };
|
|
572
|
+
}
|
|
573
|
+
return compartment;
|
|
574
|
+
});
|
|
432
575
|
}
|
|
576
|
+
patchedNodes[id] = next;
|
|
577
|
+
}
|
|
578
|
+
if (node.type === 'slab' || node.type === 'ceiling') {
|
|
579
|
+
patchedNodes[id] = migrateSingleMaterialSlots(patchedNodes[id], ['surface'], mintedMaterials);
|
|
580
|
+
}
|
|
581
|
+
if (node.type === 'fence') {
|
|
582
|
+
patchedNodes[id] = migrateSingleMaterialSlots(patchedNodes[id], ['posts', 'infill', 'base', 'rail'], mintedMaterials);
|
|
433
583
|
}
|
|
434
|
-
if (node.type === '
|
|
435
|
-
|
|
436
|
-
|
|
584
|
+
if (node.type === 'column') {
|
|
585
|
+
patchedNodes[id] = migrateSingleMaterialSlots(patchedNodes[id], ['shaft', 'base', 'capital', 'frame'], mintedMaterials);
|
|
586
|
+
}
|
|
587
|
+
if (node.type === 'shelf') {
|
|
588
|
+
const normalized = normalizeShelfNode(node);
|
|
437
589
|
if (normalized) {
|
|
438
590
|
patchedNodes[id] = normalized;
|
|
439
591
|
}
|
|
592
|
+
patchedNodes[id] = migrateSingleMaterialSlots(patchedNodes[id], ['shelves', 'frame', 'back'], mintedMaterials);
|
|
440
593
|
}
|
|
441
594
|
// Roof-segment hosting was added in this migration cycle (the same
|
|
442
595
|
// pattern as shelf above). Older segments saved before the schema
|
|
@@ -449,6 +602,41 @@ function migrateNodes(nodes) {
|
|
|
449
602
|
if (node.type === 'roof-segment' && !Array.isArray(node.children)) {
|
|
450
603
|
patchedNodes[id] = { ...node, children: [] };
|
|
451
604
|
}
|
|
605
|
+
// Roof-hosted wall children (door / window / item) originally stored
|
|
606
|
+
// SEGMENT-LOCAL positions with the face yaw in rotation[1]; the
|
|
607
|
+
// format moved to explicit `roofFace` + FACE-LOCAL coords so the
|
|
608
|
+
// renderer's face frame can track segment edits live. Convert in
|
|
609
|
+
// place: face from the old cardinal yaw, u/v from the outer-plane
|
|
610
|
+
// projection, z re-based from the outer plane to the wall mid-plane.
|
|
611
|
+
if ((node.type === 'door' || node.type === 'window' || node.type === 'item') &&
|
|
612
|
+
typeof node.roofSegmentId === 'string' &&
|
|
613
|
+
node.roofFace === undefined) {
|
|
614
|
+
const current = patchedNodes[id];
|
|
615
|
+
const segment = patchedNodes[current.roofSegmentId];
|
|
616
|
+
if (segment?.type === 'roof-segment') {
|
|
617
|
+
const tau = Math.PI * 2;
|
|
618
|
+
const yaw = (((current.rotation?.[1] ?? 0) % tau) + tau) % tau;
|
|
619
|
+
const eps = 1e-3;
|
|
620
|
+
const face = yaw < eps || tau - yaw < eps
|
|
621
|
+
? 'front'
|
|
622
|
+
: Math.abs(yaw - Math.PI) < eps
|
|
623
|
+
? 'back'
|
|
624
|
+
: Math.abs(yaw - Math.PI / 2) < eps
|
|
625
|
+
? 'right'
|
|
626
|
+
: Math.abs(yaw - (3 * Math.PI) / 2) < eps
|
|
627
|
+
? 'left'
|
|
628
|
+
: null;
|
|
629
|
+
if (face) {
|
|
630
|
+
const { u, v, dist } = segmentPointToRoofWallFace(segment, face, current.position);
|
|
631
|
+
patchedNodes[id] = {
|
|
632
|
+
...current,
|
|
633
|
+
roofFace: face,
|
|
634
|
+
position: [u, v, dist + (segment.wallThickness ?? 0.1) / 2],
|
|
635
|
+
rotation: [0, 0, 0],
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
452
640
|
if (node.type === 'roof') {
|
|
453
641
|
patchedNodes[id] = migrateRoofSurfaceMaterials(patchedNodes[id]);
|
|
454
642
|
}
|
|
@@ -476,8 +664,52 @@ function migrateNodes(nodes) {
|
|
|
476
664
|
patchedNodes[id] = { ...node, children: flattened };
|
|
477
665
|
}
|
|
478
666
|
}
|
|
667
|
+
// Level children normalization.
|
|
668
|
+
// Pre-0.9.1 JSONs may carry child IDs that no longer exist in the node
|
|
669
|
+
// map (e.g. elevator IDs that lived under a level before the elevator
|
|
670
|
+
// parent migration moved them up to building). If those dangling IDs are
|
|
671
|
+
// left in place, collectReachableNodeIds marks the level as having
|
|
672
|
+
// reachable children that don't exist, which corrupts the scene graph
|
|
673
|
+
// traversal and leaves the LevelNode in a broken state — making floors
|
|
674
|
+
// impossible to drag or delete after import.
|
|
675
|
+
// We intentionally do NOT filter by type prefix here; being permissive
|
|
676
|
+
// about which types are allowed as children prevents data loss when new
|
|
677
|
+
// child types are added to the schema in the future.
|
|
678
|
+
if (node.type === 'level') {
|
|
679
|
+
const rawChildren = getStringArray(node.children);
|
|
680
|
+
const validChildren = rawChildren.filter((childId) => {
|
|
681
|
+
const exists = Boolean(patchedNodes[childId]);
|
|
682
|
+
if (!exists) {
|
|
683
|
+
console.warn('[migrateNodes] level', id, 'references missing child', childId, '— dropping');
|
|
684
|
+
}
|
|
685
|
+
return exists;
|
|
686
|
+
});
|
|
687
|
+
const levelNumber = getFiniteNumber(node.level, 0);
|
|
688
|
+
patchedNodes[id] = {
|
|
689
|
+
...node,
|
|
690
|
+
level: levelNumber,
|
|
691
|
+
children: validChildren,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
// Pass 2: elevator migration.
|
|
696
|
+
// migrateElevatorParent mutates the parent level's children array (removes
|
|
697
|
+
// the elevator ID from it). Running this after Pass 1 guarantees that the
|
|
698
|
+
// level normalization above has already seen a clean children list — if we
|
|
699
|
+
// ran elevator migration inside Pass 1, the order of Object.entries
|
|
700
|
+
// iteration would be non-deterministic: processing an elevator before its
|
|
701
|
+
// parent level would mutate the level's children mid-iteration, potentially
|
|
702
|
+
// causing the level branch above to see a stale node reference.
|
|
703
|
+
for (const [id, node] of Object.entries(patchedNodes)) {
|
|
704
|
+
if (node.type !== 'elevator')
|
|
705
|
+
continue;
|
|
706
|
+
const parentMigrated = migrateElevatorParent(id, node, patchedNodes);
|
|
707
|
+
const normalized = normalizeElevatorNode(parentMigrated);
|
|
708
|
+
if (normalized) {
|
|
709
|
+
patchedNodes[id] = normalized;
|
|
710
|
+
}
|
|
479
711
|
}
|
|
480
|
-
return patchedNodes;
|
|
712
|
+
return { nodes: patchedNodes, mintedMaterials };
|
|
481
713
|
}
|
|
482
714
|
function getNodeChildIds(node) {
|
|
483
715
|
if (!('children' in node && Array.isArray(node.children))) {
|
|
@@ -536,6 +768,9 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
536
768
|
dirtyNodes: new Set(),
|
|
537
769
|
// 4. Collections
|
|
538
770
|
collections: {},
|
|
771
|
+
materials: {},
|
|
772
|
+
installedPlugins: [],
|
|
773
|
+
hasExplicitPluginInstallState: false,
|
|
539
774
|
// 5. Read-only lock
|
|
540
775
|
readOnly: false,
|
|
541
776
|
setReadOnly: (readOnly) => set({ readOnly }),
|
|
@@ -545,15 +780,25 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
545
780
|
rootNodeIds: [],
|
|
546
781
|
dirtyNodes: new Set(),
|
|
547
782
|
collections: {},
|
|
783
|
+
materials: {},
|
|
784
|
+
installedPlugins: [],
|
|
785
|
+
hasExplicitPluginInstallState: false,
|
|
548
786
|
});
|
|
549
787
|
},
|
|
550
788
|
clearScene: () => {
|
|
789
|
+
const installedPlugins = get().installedPlugins;
|
|
790
|
+
const hasExplicitPluginInstallState = get().hasExplicitPluginInstallState;
|
|
551
791
|
get().unloadScene();
|
|
552
792
|
get().loadScene(); // Default scene
|
|
793
|
+
set({ installedPlugins, hasExplicitPluginInstallState });
|
|
553
794
|
},
|
|
554
|
-
setScene: (nodes, rootNodeIds) => {
|
|
795
|
+
setScene: (nodes, rootNodeIds, extra) => {
|
|
555
796
|
// Apply backward compatibility migrations
|
|
556
|
-
const patchedNodes = migrateNodes(nodes);
|
|
797
|
+
const { nodes: patchedNodes, mintedMaterials } = migrateNodes(nodes);
|
|
798
|
+
// Scene materials minted by the wall legacy→slots migration join the
|
|
799
|
+
// loaded palette (existing refs win on id collision — there are none,
|
|
800
|
+
// ids are freshly generated).
|
|
801
|
+
const materials = { ...mintedMaterials, ...(extra?.materials ?? {}) };
|
|
557
802
|
// Remove orphans: nodes whose parentId points to a non-existent node
|
|
558
803
|
const cleanedNodes = { ...patchedNodes };
|
|
559
804
|
for (const node of Object.values(cleanedNodes)) {
|
|
@@ -562,12 +807,6 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
562
807
|
delete cleanedNodes[node.id];
|
|
563
808
|
}
|
|
564
809
|
}
|
|
565
|
-
set({
|
|
566
|
-
nodes: cleanedNodes,
|
|
567
|
-
rootNodeIds,
|
|
568
|
-
dirtyNodes: new Set(),
|
|
569
|
-
collections: {},
|
|
570
|
-
});
|
|
571
810
|
const normalizedRootNodeIds = normalizeRootNodeIds(cleanedNodes, rootNodeIds);
|
|
572
811
|
const reachableNodeIds = collectReachableNodeIds(cleanedNodes, normalizedRootNodeIds);
|
|
573
812
|
if (normalizedRootNodeIds.length > 0) {
|
|
@@ -578,17 +817,47 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
578
817
|
delete cleanedNodes[node.id];
|
|
579
818
|
}
|
|
580
819
|
}
|
|
820
|
+
// Single tracked `set`: with zundo, every tracked write pushes the
|
|
821
|
+
// pre-write state onto `pastStates`. Writing the scene in two steps
|
|
822
|
+
// (as this used to) exposed a half-normalized intermediate state —
|
|
823
|
+
// and the pre-load (possibly empty) state — as undo targets.
|
|
581
824
|
set({
|
|
582
825
|
nodes: cleanedNodes,
|
|
583
826
|
rootNodeIds: normalizedRootNodeIds,
|
|
584
827
|
dirtyNodes: new Set(),
|
|
585
|
-
collections: {},
|
|
828
|
+
collections: extra?.collections ?? {},
|
|
829
|
+
materials,
|
|
830
|
+
installedPlugins: Array.from(new Set(extra?.installedPlugins ?? [])),
|
|
831
|
+
hasExplicitPluginInstallState: extra?.hasExplicitPluginInstallState ?? false,
|
|
586
832
|
});
|
|
587
833
|
// Mark all nodes as dirty to trigger re-validation
|
|
588
834
|
Object.values(cleanedNodes).forEach((node) => {
|
|
589
835
|
get().markDirty(node.id);
|
|
590
836
|
});
|
|
591
837
|
},
|
|
838
|
+
setInstalledPlugins: (pluginIds, options) => {
|
|
839
|
+
if (get().readOnly)
|
|
840
|
+
return;
|
|
841
|
+
const nextInstalledPlugins = Array.from(new Set(pluginIds));
|
|
842
|
+
const previousInstalledPlugins = get().installedPlugins;
|
|
843
|
+
const dirtyNodes = new Set(get().dirtyNodes);
|
|
844
|
+
for (const node of Object.values(get().nodes)) {
|
|
845
|
+
if (!getNodePluginId(node.type))
|
|
846
|
+
continue;
|
|
847
|
+
if (!isNodeKindEnabled(node.type, nextInstalledPlugins)) {
|
|
848
|
+
dirtyNodes.delete(node.id);
|
|
849
|
+
}
|
|
850
|
+
else if (!isNodeKindEnabled(node.type, previousInstalledPlugins)) {
|
|
851
|
+
if (nodeRegistry.get(node.type)?.dirtyTracking !== false)
|
|
852
|
+
dirtyNodes.add(node.id);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
set({
|
|
856
|
+
installedPlugins: nextInstalledPlugins,
|
|
857
|
+
hasExplicitPluginInstallState: options?.explicit ?? get().hasExplicitPluginInstallState,
|
|
858
|
+
dirtyNodes,
|
|
859
|
+
});
|
|
860
|
+
},
|
|
592
861
|
loadScene: () => {
|
|
593
862
|
if (get().rootNodeIds.length > 0) {
|
|
594
863
|
// Assign all nodes as dirty to force re-validation
|
|
@@ -619,6 +888,11 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
619
888
|
set({ nodes, rootNodeIds });
|
|
620
889
|
},
|
|
621
890
|
markDirty: (id) => {
|
|
891
|
+
const node = get().nodes[id];
|
|
892
|
+
if (node && !isNodeKindEnabled(node.type, get().installedPlugins))
|
|
893
|
+
return;
|
|
894
|
+
if (node && nodeRegistry.get(node.type)?.dirtyTracking === false)
|
|
895
|
+
return;
|
|
622
896
|
get().dirtyNodes.add(id);
|
|
623
897
|
},
|
|
624
898
|
clearDirty: (id) => {
|
|
@@ -730,10 +1004,37 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
730
1004
|
return { collections: nextCollections, nodes: nextNodes };
|
|
731
1005
|
});
|
|
732
1006
|
},
|
|
1007
|
+
// --- SCENE MATERIALS ---
|
|
1008
|
+
addSceneMaterial: (material) => {
|
|
1009
|
+
if (get().readOnly)
|
|
1010
|
+
return;
|
|
1011
|
+
set((state) => ({
|
|
1012
|
+
materials: { ...state.materials, [material.id]: material },
|
|
1013
|
+
}));
|
|
1014
|
+
},
|
|
1015
|
+
updateSceneMaterial: (id, data) => {
|
|
1016
|
+
if (get().readOnly)
|
|
1017
|
+
return;
|
|
1018
|
+
set((state) => {
|
|
1019
|
+
const material = state.materials[id];
|
|
1020
|
+
if (!material)
|
|
1021
|
+
return state;
|
|
1022
|
+
return { materials: { ...state.materials, [id]: { ...material, ...data } } };
|
|
1023
|
+
});
|
|
1024
|
+
},
|
|
1025
|
+
removeSceneMaterial: (id) => {
|
|
1026
|
+
if (get().readOnly)
|
|
1027
|
+
return;
|
|
1028
|
+
set((state) => {
|
|
1029
|
+
const materials = { ...state.materials };
|
|
1030
|
+
delete materials[id];
|
|
1031
|
+
return { materials };
|
|
1032
|
+
});
|
|
1033
|
+
},
|
|
733
1034
|
}), {
|
|
734
1035
|
partialize: (state) => {
|
|
735
|
-
const { nodes, rootNodeIds, collections } = state;
|
|
736
|
-
return { nodes, rootNodeIds, collections };
|
|
1036
|
+
const { nodes, rootNodeIds, collections, materials, installedPlugins } = state;
|
|
1037
|
+
return { nodes, rootNodeIds, collections, materials, installedPlugins };
|
|
737
1038
|
},
|
|
738
1039
|
limit: 50, // Limit to last 50 actions
|
|
739
1040
|
}));
|
|
@@ -744,6 +1045,12 @@ let prevFutureLength = 0;
|
|
|
744
1045
|
let prevNodesSnapshot = null;
|
|
745
1046
|
export function clearSceneHistory() {
|
|
746
1047
|
resetSceneHistoryPauseDepth();
|
|
1048
|
+
// Resetting the pause-depth counter without resuming would strand the
|
|
1049
|
+
// temporal store in `isTracking: false` if a pause window was active when
|
|
1050
|
+
// the scene was (re)loaded — every edit after the load would then be
|
|
1051
|
+
// invisible to undo. Resume unconditionally so the cleared history starts
|
|
1052
|
+
// tracking from the loaded baseline.
|
|
1053
|
+
useScene.temporal.getState().resume();
|
|
747
1054
|
useScene.temporal.getState().clear();
|
|
748
1055
|
prevPastLength = 0;
|
|
749
1056
|
prevFutureLength = 0;
|
|
@@ -51,7 +51,7 @@ export function resolveElevatorServiceLevels(elevator, nodes) {
|
|
|
51
51
|
}
|
|
52
52
|
export function getElevatorLevelHeight(levelId, nodes) {
|
|
53
53
|
const level = nodes[levelId];
|
|
54
|
-
if (
|
|
54
|
+
if (level?.type !== 'level')
|
|
55
55
|
return DEFAULT_ELEVATOR_LEVEL_HEIGHT;
|
|
56
56
|
let maxTop = 0;
|
|
57
57
|
for (const childId of level.children) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FenceNode } from '../../schema';
|
|
2
|
+
import type { Point2D } from '../wall/wall-mitering';
|
|
3
|
+
type CurveFrame = {
|
|
4
|
+
point: Point2D;
|
|
5
|
+
tangent: Point2D;
|
|
6
|
+
normal: Point2D;
|
|
7
|
+
};
|
|
8
|
+
export declare function getFenceCenterlineFrameAt(fence: FenceNode, t: number): CurveFrame;
|
|
9
|
+
export declare function sampleFenceCenterline(fence: FenceNode, segments?: number): Point2D[];
|
|
10
|
+
export declare function getFenceCenterlineLength(fence: FenceNode, segments?: number): number;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=fence-centerline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fence-centerline.d.ts","sourceRoot":"","sources":["../../../src/systems/fence/fence-centerline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAqBpD,KAAK,UAAU,GAAG;IAChB,KAAK,EAAE,OAAO,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,OAAO,CAAA;CAChB,CAAA;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,UAAU,CAKjF;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,SAAS,EAChB,QAAQ,SAA0B,GACjC,OAAO,EAAE,CASX;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,SAAS,EAChB,QAAQ,SAA0B,GACjC,MAAM,CAOR"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getWallCurveFrameAt, getWallCurveLength, sampleWallCenterline } from '../wall/wall-curve';
|
|
2
|
+
import { getFenceSplineFrameAt, getFenceSplineLength, isSplineFence, sampleFenceSpline, } from './fence-spline';
|
|
3
|
+
/**
|
|
4
|
+
* Unified fence centerline accessors. A fence is either:
|
|
5
|
+
* - a spline fence (`path` of >= 2 control points) → smooth Catmull-Rom, or
|
|
6
|
+
* - a straight / single-arc fence (`start`/`end` + optional `curveOffset`).
|
|
7
|
+
*
|
|
8
|
+
* These wrappers branch on `isSplineFence` and return the SAME shapes the wall
|
|
9
|
+
* arc helpers return, so every consumer (3D geometry, 2D floor-plan, length,
|
|
10
|
+
* handles) can sample the centerline without caring which kind it is. Wall arc
|
|
11
|
+
* math in `wall-curve.ts` is untouched — walls never carry a `path`.
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_SAMPLE_SEGMENTS = 96;
|
|
14
|
+
export function getFenceCenterlineFrameAt(fence, t) {
|
|
15
|
+
if (isSplineFence(fence) && fence.path) {
|
|
16
|
+
return getFenceSplineFrameAt(fence.path, t, fence.tangents);
|
|
17
|
+
}
|
|
18
|
+
return getWallCurveFrameAt(fence, t);
|
|
19
|
+
}
|
|
20
|
+
export function sampleFenceCenterline(fence, segments = DEFAULT_SAMPLE_SEGMENTS) {
|
|
21
|
+
if (isSplineFence(fence) && fence.path) {
|
|
22
|
+
// Spread the requested sample budget across the spans so a long path still
|
|
23
|
+
// reads smoothly without exploding the point count.
|
|
24
|
+
const spanCount = Math.max(1, fence.path.length - 1);
|
|
25
|
+
const perSpan = Math.max(2, Math.ceil(segments / spanCount));
|
|
26
|
+
return sampleFenceSpline(fence.path, fence.tangents, perSpan);
|
|
27
|
+
}
|
|
28
|
+
return sampleWallCenterline(fence, segments);
|
|
29
|
+
}
|
|
30
|
+
export function getFenceCenterlineLength(fence, segments = DEFAULT_SAMPLE_SEGMENTS) {
|
|
31
|
+
if (isSplineFence(fence) && fence.path) {
|
|
32
|
+
const spanCount = Math.max(1, fence.path.length - 1);
|
|
33
|
+
const perSpan = Math.max(2, Math.ceil(segments / spanCount));
|
|
34
|
+
return getFenceSplineLength(fence.path, fence.tangents, perSpan);
|
|
35
|
+
}
|
|
36
|
+
return getWallCurveLength(fence, segments);
|
|
37
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { FenceNode } from '../../schema';
|
|
2
|
+
import type { Point2D } from '../wall/wall-mitering';
|
|
3
|
+
type FenceSplineLike = Pick<FenceNode, 'path'>;
|
|
4
|
+
type TangentList = ReadonlyArray<readonly [number, number] | null> | undefined;
|
|
5
|
+
export declare function isSplineFence(fence: FenceSplineLike): boolean;
|
|
6
|
+
type CurveFrame = {
|
|
7
|
+
point: Point2D;
|
|
8
|
+
tangent: Point2D;
|
|
9
|
+
normal: Point2D;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* OUT-handle offset vector for control point `index` — the stored tangent if
|
|
13
|
+
* the user has adjusted it, otherwise the automatic distance-aware tangent
|
|
14
|
+
* (endpoints duplicate the neighbour so the ends stay tangent to their single
|
|
15
|
+
* span). The IN handle is the negation of this.
|
|
16
|
+
*
|
|
17
|
+
* Exported so the editing UI can draw the tangent line / handle dots at the
|
|
18
|
+
* right place even before the user has dragged them.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getFenceControlHandle(path: ReadonlyArray<readonly [number, number]>, tangents: TangentList, index: number): Point2D;
|
|
21
|
+
export declare function getTwoPointFenceCurveTangents(path: ReadonlyArray<readonly [number, number]>): Array<[number, number] | null> | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Sample the spline centerline into a polyline. Control points are honored as
|
|
24
|
+
* on-curve anchors; `segmentsPerSpan` controls smoothness between them.
|
|
25
|
+
* Returns `(segmentsPerSpan * spanCount) + 1` points, first == path[0],
|
|
26
|
+
* last == path[-1].
|
|
27
|
+
*/
|
|
28
|
+
export declare function sampleFenceSpline(path: ReadonlyArray<readonly [number, number]>, tangents?: TangentList, segmentsPerSpan?: number): Point2D[];
|
|
29
|
+
/**
|
|
30
|
+
* Frame (point + tangent + normal) at parameter `t` in [0, 1] along the spline
|
|
31
|
+
* centerline. Same return shape as `getWallCurveFrameAt` so it is a drop-in for
|
|
32
|
+
* the arc branch. `t` is uniform over the sampled polyline (arc length is not
|
|
33
|
+
* reparameterised — adequate for marching posts / rails and far cheaper).
|
|
34
|
+
*/
|
|
35
|
+
export declare function getFenceSplineFrameAt(path: ReadonlyArray<readonly [number, number]>, t: number, tangents?: TangentList, segmentsPerSpan?: number): CurveFrame;
|
|
36
|
+
/** Total polyline length of the sampled spline centerline. */
|
|
37
|
+
export declare function getFenceSplineLength(path: ReadonlyArray<readonly [number, number]>, tangents?: TangentList, segmentsPerSpan?: number): number;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=fence-spline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fence-spline.d.ts","sourceRoot":"","sources":["../../../src/systems/fence/fence-spline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AA0BpD,KAAK,eAAe,GAAG,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;AAC9C,KAAK,WAAW,GAAG,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,SAAS,CAAA;AAE9E,wBAAgB,aAAa,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAE7D;AAED,KAAK,UAAU,GAAG;IAChB,KAAK,EAAE,OAAO,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,OAAO,CAAA;CAChB,CAAA;AAkBD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAC9C,QAAQ,EAAE,WAAW,EACrB,KAAK,EAAE,MAAM,GACZ,OAAO,CAiCT;AAED,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAC7C,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,SAAS,CAyB5C;AAkBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAC9C,QAAQ,CAAC,EAAE,WAAW,EACtB,eAAe,SAA4B,GAC1C,OAAO,EAAE,CA0BX;AA4CD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAC9C,CAAC,EAAE,MAAM,EACT,QAAQ,CAAC,EAAE,WAAW,EACtB,eAAe,SAA4B,GAC1C,UAAU,CAEZ;AAED,8DAA8D;AAC9D,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAC9C,QAAQ,CAAC,EAAE,WAAW,EACtB,eAAe,SAA4B,GAC1C,MAAM,CAOR"}
|