@pascal-app/core 1.0.0-beta.5 → 1.0.0
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/LICENSE +21 -0
- package/README.md +23 -0
- package/dist/capture/index.d.ts +3 -0
- package/dist/capture/index.d.ts.map +1 -0
- package/dist/capture/index.js +2 -0
- package/dist/capture/schema.d.ts +417 -0
- package/dist/capture/schema.d.ts.map +1 -0
- package/dist/capture/schema.js +348 -0
- package/dist/capture/source.d.ts +60 -0
- package/dist/capture/source.d.ts.map +1 -0
- package/dist/capture/source.js +254 -0
- package/dist/events/bus.d.ts +55 -7
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/hidden-wall-pointer-hold.d.ts +28 -0
- package/dist/events/hidden-wall-pointer-hold.d.ts.map +1 -0
- package/dist/events/hidden-wall-pointer-hold.js +37 -0
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +2785 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +132 -19
- package/dist/hooks/spatial-grid/support-host-patch.d.ts +45 -1
- package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/support-host-patch.js +171 -6
- package/dist/index.d.ts +14 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -5
- package/dist/lib/asset-storage.d.ts.map +1 -1
- package/dist/lib/asset-storage.js +4 -1
- package/dist/lib/polygon-union.d.ts +4 -0
- package/dist/lib/polygon-union.d.ts.map +1 -0
- package/dist/lib/polygon-union.js +306 -0
- package/dist/lib/roof-overlap.d.ts +32 -0
- package/dist/lib/roof-overlap.d.ts.map +1 -0
- package/dist/lib/roof-overlap.js +72 -0
- package/dist/lib/slab-polygon.d.ts +35 -2
- package/dist/lib/slab-polygon.d.ts.map +1 -1
- package/dist/lib/slab-polygon.js +133 -27
- package/dist/lib/space-detection.d.ts +11 -0
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +42 -14
- package/dist/registry/handles.d.ts +24 -3
- package/dist/registry/handles.d.ts.map +1 -1
- package/dist/registry/index.d.ts +3 -4
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +1 -2
- package/dist/registry/registry.d.ts +13 -6
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/registry.js +37 -4
- package/dist/registry/scene-api.d.ts +16 -0
- package/dist/registry/scene-api.d.ts.map +1 -1
- package/dist/registry/scene-api.js +49 -1
- package/dist/registry/types.d.ts +226 -13
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/schema/__bench__/node-parsers.bench.d.ts +24 -0
- package/dist/schema/__bench__/node-parsers.bench.d.ts.map +1 -0
- package/dist/schema/__bench__/node-parsers.bench.js +314 -0
- package/dist/schema/__fixtures__/node-fixtures.d.ts +28 -0
- package/dist/schema/__fixtures__/node-fixtures.d.ts.map +1 -0
- package/dist/schema/__fixtures__/node-fixtures.js +125 -0
- package/dist/schema/base.d.ts +1 -1
- package/dist/schema/base.d.ts.map +1 -1
- package/dist/schema/base.js +8 -1
- package/dist/schema/compiled-node-parsers.d.ts +40 -0
- package/dist/schema/compiled-node-parsers.d.ts.map +1 -0
- package/dist/schema/compiled-node-parsers.js +95 -0
- package/dist/schema/index.d.ts +19 -15
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +17 -12
- package/dist/schema/material.d.ts +2 -1
- package/dist/schema/material.d.ts.map +1 -1
- package/dist/schema/material.js +1 -0
- package/dist/schema/nodes/automatic-downspout.d.ts +17 -0
- package/dist/schema/nodes/automatic-downspout.d.ts.map +1 -0
- package/dist/schema/nodes/automatic-downspout.js +237 -0
- package/dist/schema/nodes/block.d.ts +107 -0
- package/dist/schema/nodes/block.d.ts.map +1 -0
- package/dist/schema/nodes/block.js +207 -0
- package/dist/schema/nodes/box-vent.d.ts +8 -2
- package/dist/schema/nodes/box-vent.d.ts.map +1 -1
- package/dist/schema/nodes/box-vent.js +2 -0
- package/dist/schema/nodes/building.d.ts +1 -1
- package/dist/schema/nodes/cabinet.d.ts +25 -3
- package/dist/schema/nodes/cabinet.d.ts.map +1 -1
- package/dist/schema/nodes/cabinet.js +26 -5
- package/dist/schema/nodes/ceiling.d.ts +1 -1
- package/dist/schema/nodes/chimney.d.ts +2 -2
- package/dist/schema/nodes/column.d.ts +6 -6
- package/dist/schema/nodes/construction-dimension.d.ts +4 -4
- package/dist/schema/nodes/cupola.d.ts +9 -1
- package/dist/schema/nodes/cupola.d.ts.map +1 -1
- package/dist/schema/nodes/cupola.js +2 -0
- package/dist/schema/nodes/door.d.ts +1 -1
- package/dist/schema/nodes/dormer.d.ts +44 -1
- package/dist/schema/nodes/dormer.d.ts.map +1 -1
- package/dist/schema/nodes/dormer.js +131 -7
- package/dist/schema/nodes/downspout.d.ts +11 -1
- package/dist/schema/nodes/downspout.d.ts.map +1 -1
- package/dist/schema/nodes/downspout.js +24 -0
- package/dist/schema/nodes/duct-fitting.d.ts +18 -1
- package/dist/schema/nodes/duct-fitting.d.ts.map +1 -1
- package/dist/schema/nodes/duct-fitting.js +25 -4
- package/dist/schema/nodes/duct-segment.d.ts +23 -1
- package/dist/schema/nodes/duct-segment.d.ts.map +1 -1
- package/dist/schema/nodes/duct-segment.js +20 -0
- package/dist/schema/nodes/duct-terminal.d.ts +1 -1
- package/dist/schema/nodes/elevator.d.ts +1 -1
- package/dist/schema/nodes/eyebrow-vent.d.ts +8 -2
- package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -1
- package/dist/schema/nodes/eyebrow-vent.js +2 -0
- package/dist/schema/nodes/fence.d.ts +1 -1
- package/dist/schema/nodes/guide.d.ts +1 -1
- package/dist/schema/nodes/gutter.d.ts +36 -2
- package/dist/schema/nodes/gutter.d.ts.map +1 -1
- package/dist/schema/nodes/gutter.js +308 -1
- package/dist/schema/nodes/hanger-overrides.d.ts +7 -0
- package/dist/schema/nodes/hanger-overrides.d.ts.map +1 -0
- package/dist/schema/nodes/hanger-overrides.js +6 -0
- package/dist/schema/nodes/hvac-equipment.d.ts +1 -1
- package/dist/schema/nodes/item.d.ts +4 -3
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +5 -0
- package/dist/schema/nodes/lean-to-extension.d.ts +212 -0
- package/dist/schema/nodes/lean-to-extension.d.ts.map +1 -0
- package/dist/schema/nodes/lean-to-extension.js +114 -0
- package/dist/schema/nodes/level.d.ts +2 -2
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/lineset.d.ts +1 -1
- package/dist/schema/nodes/liquid-line.d.ts +1 -1
- package/dist/schema/nodes/measurement.d.ts +1 -1
- package/dist/schema/nodes/pipe-fitting.d.ts +9 -1
- package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -1
- package/dist/schema/nodes/pipe-fitting.js +6 -2
- package/dist/schema/nodes/pipe-segment.d.ts +23 -1
- package/dist/schema/nodes/pipe-segment.d.ts.map +1 -1
- package/dist/schema/nodes/pipe-segment.js +20 -0
- package/dist/schema/nodes/pipe-trap.d.ts +1 -1
- package/dist/schema/nodes/ridge-vent.d.ts +1 -1
- package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
- package/dist/schema/nodes/ridge-vent.js +4 -1
- package/dist/schema/nodes/roof-segment-shape.d.ts +4 -0
- package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment-shape.js +64 -1
- package/dist/schema/nodes/roof-segment-walls.d.ts +3 -1
- package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment-walls.js +1 -1
- package/dist/schema/nodes/roof-segment.d.ts +41 -1
- package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment.js +79 -2
- package/dist/schema/nodes/roof.d.ts +22 -1
- package/dist/schema/nodes/roof.d.ts.map +1 -1
- package/dist/schema/nodes/roof.js +14 -0
- package/dist/schema/nodes/scan.d.ts +19 -2
- package/dist/schema/nodes/scan.d.ts.map +1 -1
- package/dist/schema/nodes/scan.js +11 -1
- package/dist/schema/nodes/shelf.d.ts +2 -2
- package/dist/schema/nodes/site.d.ts +1 -1
- package/dist/schema/nodes/skylight.d.ts +1 -1
- package/dist/schema/nodes/slab.d.ts +1 -1
- package/dist/schema/nodes/solar-panel.d.ts +1 -1
- package/dist/schema/nodes/spawn.d.ts +1 -1
- package/dist/schema/nodes/stair-segment.d.ts +1 -1
- package/dist/schema/nodes/stair.d.ts +1 -1
- package/dist/schema/nodes/structural-grid.d.ts +1 -1
- package/dist/schema/nodes/turbine-vent.d.ts +7 -1
- package/dist/schema/nodes/turbine-vent.d.ts.map +1 -1
- package/dist/schema/nodes/turbine-vent.js +2 -0
- package/dist/schema/nodes/wall.d.ts +4 -4
- package/dist/schema/nodes/wall.d.ts.map +1 -1
- package/dist/schema/nodes/wall.js +7 -1
- package/dist/schema/nodes/window.d.ts +10 -3
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/nodes/window.js +4 -0
- package/dist/schema/nodes/zone.d.ts +2 -2
- package/dist/schema/nodes/zone.js +1 -1
- package/dist/schema/types.d.ts +484 -109
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +27 -1
- package/dist/services/hosting.d.ts +2 -2
- package/dist/services/hosting.d.ts.map +1 -1
- package/dist/services/hosting.js +4 -4
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/system-graph.d.ts +11 -0
- package/dist/services/system-graph.d.ts.map +1 -1
- package/dist/services/system-graph.js +35 -5
- package/dist/store/actions/node-actions.d.ts +7 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +429 -17
- package/dist/store/history-invalidation.d.ts +3 -0
- package/dist/store/history-invalidation.d.ts.map +1 -0
- package/dist/store/history-invalidation.js +72 -0
- package/dist/store/scene-hydration.d.ts +5 -0
- package/dist/store/scene-hydration.d.ts.map +1 -0
- package/dist/store/scene-hydration.js +57 -0
- package/dist/store/use-scene.d.ts +3 -0
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +332 -64
- package/dist/systems/roof/roof-elevation-system.d.ts +3 -0
- package/dist/systems/roof/roof-elevation-system.d.ts.map +1 -0
- package/dist/systems/roof/roof-elevation-system.js +94 -0
- package/dist/systems/roof/roof-elevation.d.ts +4 -0
- package/dist/systems/roof/roof-elevation.d.ts.map +1 -0
- package/dist/systems/roof/roof-elevation.js +95 -0
- package/dist/systems/roof/roof-footprint.d.ts +17 -0
- package/dist/systems/roof/roof-footprint.d.ts.map +1 -0
- package/dist/systems/roof/roof-footprint.js +83 -0
- package/dist/systems/stair/stair-flight.d.ts +17 -0
- package/dist/systems/stair/stair-flight.d.ts.map +1 -0
- package/dist/systems/stair/stair-flight.js +27 -0
- package/dist/systems/stair/stair-opening-preview.d.ts +1555 -1328
- package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-system.d.ts +1 -0
- package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-system.js +93 -96
- package/dist/systems/stair/stair-rise.d.ts.map +1 -1
- package/dist/systems/stair/stair-rise.js +20 -21
- package/dist/systems/wall/wall-curve.d.ts +1 -0
- package/dist/systems/wall/wall-curve.d.ts.map +1 -1
- package/dist/systems/wall/wall-curve.js +73 -0
- package/dist/systems/wall/wall-footprint.d.ts +1 -0
- package/dist/systems/wall/wall-footprint.d.ts.map +1 -1
- package/dist/systems/wall/wall-footprint.js +1 -0
- package/dist/utils/clone-scene-graph.d.ts.map +1 -1
- package/dist/utils/clone-scene-graph.js +26 -0
- package/dist/validation/validate-build-json.d.ts +3 -0
- package/dist/validation/validate-build-json.d.ts.map +1 -1
- package/dist/validation/validate-build-json.js +49 -2
- package/package.json +10 -3
- package/dist/systems/elevator/elevator-runtime-system.d.ts +0 -2
- package/dist/systems/elevator/elevator-runtime-system.d.ts.map +0 -1
- package/dist/systems/elevator/elevator-runtime-system.js +0 -8
package/dist/store/use-scene.js
CHANGED
|
@@ -5,7 +5,9 @@ import { parseMaterialRef, toSceneMaterialRef } from '../material-library';
|
|
|
5
5
|
import { getNodePluginId, isNodeKindEnabled, nodeRegistry } from '../registry/registry';
|
|
6
6
|
import { BuildingNode } from '../schema';
|
|
7
7
|
import { generateCollectionId } from '../schema/collections';
|
|
8
|
+
import { compiledNodeSchema } from '../schema/compiled-node-parsers';
|
|
8
9
|
import { DoorNode as DoorNodeSchema } from '../schema/nodes/door';
|
|
10
|
+
import { createDormerDefaultWindow, DormerNode as DormerNodeSchema, getDormerDefaultWindowFace, } from '../schema/nodes/dormer';
|
|
9
11
|
import { ElevatorNode as ElevatorNodeSchema } from '../schema/nodes/elevator';
|
|
10
12
|
import { LevelNode, normalizeLevelBaseElevation } from '../schema/nodes/level';
|
|
11
13
|
import { getPitchFromActiveRoofHeight, } from '../schema/nodes/roof-segment';
|
|
@@ -18,11 +20,16 @@ import { getEffectiveWallSurfaceMaterial } from '../schema/nodes/wall';
|
|
|
18
20
|
import { WindowNode as WindowNodeSchema } from '../schema/nodes/window';
|
|
19
21
|
import { generateSceneMaterialId, SceneMaterial, } from '../schema/scene-material';
|
|
20
22
|
import { AnyNode as AnyNodeSchema } from '../schema/types';
|
|
23
|
+
import { syncAutoElevatorOpenings } from '../systems/elevator/elevator-opening-sync';
|
|
24
|
+
import { syncAutoStairOpenings } from '../systems/stair/stair-opening-sync';
|
|
25
|
+
import { syncStairRises } from '../systems/stair/stair-rise';
|
|
21
26
|
import { healSceneNodes } from '../utils/heal-scene-graph';
|
|
22
27
|
import { removeRetiredDrawingSheetNodes } from '../utils/retired-scene-nodes';
|
|
23
28
|
import { migrateVerticalSceneNodes } from '../utils/vertical-scene-migration';
|
|
24
29
|
import * as nodeActions from './actions/node-actions';
|
|
25
|
-
import { areSceneSnapshotsEqual, getSceneHistoryPauseDepth, notifySceneCommit, pauseSceneHistory, resetSceneHistoryPauseDepth, resumeSceneHistory, } from './history-control';
|
|
30
|
+
import { areSceneSnapshotsEqual, getSceneHistoryPauseDepth, notifySceneCommit, pauseSceneHistory, resetSceneHistoryPauseDepth, resumeSceneHistory, runWithSceneCommitNodeIds, } from './history-control';
|
|
31
|
+
import { getHistoryDirtyNodeIds } from './history-invalidation';
|
|
32
|
+
import { invalidatePendingHydration, isHydrationNormalization, queueSceneNormalization, runSceneHydration, } from './scene-hydration';
|
|
26
33
|
import useLiveNodeOverrides from './use-live-node-overrides';
|
|
27
34
|
import useLiveTransforms from './use-live-transforms';
|
|
28
35
|
function getFiniteNumber(value, fallback) {
|
|
@@ -82,7 +89,7 @@ function normalizeStairNode(node) {
|
|
|
82
89
|
railingHeight: getFiniteNumber(node.railingHeight, 0.92),
|
|
83
90
|
children: getStringArray(node.children),
|
|
84
91
|
};
|
|
85
|
-
const parsed = StairNodeSchema.safeParse(sanitized);
|
|
92
|
+
const parsed = compiledNodeSchema(StairNodeSchema).safeParse(sanitized);
|
|
86
93
|
if (!parsed.success)
|
|
87
94
|
return null;
|
|
88
95
|
if (hasTotalRise)
|
|
@@ -107,18 +114,18 @@ function normalizeStairSegmentNode(node) {
|
|
|
107
114
|
fillToFloor: getBoolean(node.fillToFloor, true),
|
|
108
115
|
thickness: getFiniteNumber(node.thickness, 0.25),
|
|
109
116
|
};
|
|
110
|
-
const parsed = StairSegmentNodeSchema.safeParse(sanitized);
|
|
117
|
+
const parsed = compiledNodeSchema(StairSegmentNodeSchema).safeParse(sanitized);
|
|
111
118
|
return parsed.success ? parsed.data : null;
|
|
112
119
|
}
|
|
113
120
|
function normalizeDoorNode(node) {
|
|
114
|
-
const parsed = DoorNodeSchema.safeParse(node);
|
|
121
|
+
const parsed = compiledNodeSchema(DoorNodeSchema).safeParse(node);
|
|
115
122
|
return parsed.success ? { ...node, ...parsed.data } : null;
|
|
116
123
|
}
|
|
117
124
|
// Windows saved before a schema field existed (e.g. `columnRatios`/`rowRatios`/
|
|
118
125
|
// `frameThickness`) load without it; the mesh builder then reads undefined and
|
|
119
126
|
// throws every frame. Zod-parse on load so schema defaults land, like doors.
|
|
120
127
|
function normalizeWindowNode(node) {
|
|
121
|
-
const parsed = WindowNodeSchema.safeParse(node);
|
|
128
|
+
const parsed = compiledNodeSchema(WindowNodeSchema).safeParse(node);
|
|
122
129
|
return parsed.success ? { ...node, ...parsed.data } : null;
|
|
123
130
|
}
|
|
124
131
|
function normalizeShelfNode(node) {
|
|
@@ -139,7 +146,7 @@ function normalizeShelfNode(node) {
|
|
|
139
146
|
withBottom: getBoolean(node.withBottom, false),
|
|
140
147
|
bracketStyle: getEnumValue(node.bracketStyle, ['minimal', 'industrial', 'hidden'], 'minimal'),
|
|
141
148
|
};
|
|
142
|
-
const parsed = ShelfNodeSchema.safeParse(sanitized);
|
|
149
|
+
const parsed = compiledNodeSchema(ShelfNodeSchema).safeParse(sanitized);
|
|
143
150
|
return parsed.success ? parsed.data : null;
|
|
144
151
|
}
|
|
145
152
|
function normalizeElevatorNode(node) {
|
|
@@ -165,7 +172,7 @@ function normalizeElevatorNode(node) {
|
|
|
165
172
|
doorDurationMs: getFiniteNumber(node.doorDurationMs, 900),
|
|
166
173
|
dwellMs: getFiniteNumber(node.dwellMs, 1400),
|
|
167
174
|
};
|
|
168
|
-
const parsed = ElevatorNodeSchema.safeParse(sanitized);
|
|
175
|
+
const parsed = compiledNodeSchema(ElevatorNodeSchema).safeParse(sanitized);
|
|
169
176
|
return parsed.success ? parsed.data : null;
|
|
170
177
|
}
|
|
171
178
|
function findBuildingIdForLevel(levelId, nodes) {
|
|
@@ -295,6 +302,42 @@ function migrateSingleMaterialSlots(node, slotIds, mintedMaterials) {
|
|
|
295
302
|
slots[slotId] = ref;
|
|
296
303
|
return { ...node, slots, material: undefined, materialPreset: undefined };
|
|
297
304
|
}
|
|
305
|
+
function migrateRoleMaterialSlots(node, roles, mintedMaterials) {
|
|
306
|
+
const slots = { ...(node.slots ?? {}) };
|
|
307
|
+
const next = { ...node };
|
|
308
|
+
let changed = false;
|
|
309
|
+
for (const role of roles) {
|
|
310
|
+
if (slots[role] === undefined) {
|
|
311
|
+
const ref = legacySpecToMaterialRef({
|
|
312
|
+
material: node[`${role}Material`] ?? node.material,
|
|
313
|
+
materialPreset: node[`${role}MaterialPreset`] ?? node.materialPreset,
|
|
314
|
+
}, mintedMaterials);
|
|
315
|
+
if (ref) {
|
|
316
|
+
slots[role] = ref;
|
|
317
|
+
changed = true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (`${role}Material` in next || `${role}MaterialPreset` in next)
|
|
321
|
+
changed = true;
|
|
322
|
+
delete next[`${role}Material`];
|
|
323
|
+
delete next[`${role}MaterialPreset`];
|
|
324
|
+
}
|
|
325
|
+
return changed ? { ...next, slots } : node;
|
|
326
|
+
}
|
|
327
|
+
function migrateRenamedSlot(node, previousId, nextId) {
|
|
328
|
+
if (!node.slots || node.slots[previousId] === undefined)
|
|
329
|
+
return node;
|
|
330
|
+
const slots = { ...node.slots };
|
|
331
|
+
if (slots[nextId] === undefined)
|
|
332
|
+
slots[nextId] = slots[previousId];
|
|
333
|
+
delete slots[previousId];
|
|
334
|
+
return { ...node, slots };
|
|
335
|
+
}
|
|
336
|
+
function migrateCupolaLouverSlot(node) {
|
|
337
|
+
if (!node.slots || node.slots.louvers !== undefined || node.slots.body === undefined)
|
|
338
|
+
return node;
|
|
339
|
+
return { ...node, slots: { ...node.slots, louvers: node.slots.body } };
|
|
340
|
+
}
|
|
298
341
|
// Stair carries per-role legacy fields (`treadMaterial*` / `sideMaterial*` /
|
|
299
342
|
// `railingMaterial*`) plus a catch-all. Map each to its slot via the same
|
|
300
343
|
// fallback chain the renderer uses (`getEffectiveStairSurfaceMaterial`):
|
|
@@ -482,6 +525,39 @@ function migrateWallAssembly(node) {
|
|
|
482
525
|
const { assemblyLayers: _assemblyLayers, ...wall } = node;
|
|
483
526
|
return assemblyThickness > 0 ? { ...wall, thickness: assemblyThickness } : wall;
|
|
484
527
|
}
|
|
528
|
+
function migrateBlockRename(id, node, nodes) {
|
|
529
|
+
if (node.type !== 'custom-mesh')
|
|
530
|
+
return [id, node];
|
|
531
|
+
const desiredId = id.startsWith('custom-mesh_') ? `block_${id.slice('custom-mesh_'.length)}` : id;
|
|
532
|
+
let nextId = desiredId;
|
|
533
|
+
let suffix = 1;
|
|
534
|
+
while (nextId !== id && nodes[nextId]) {
|
|
535
|
+
nextId = `${desiredId}_${suffix}`;
|
|
536
|
+
suffix += 1;
|
|
537
|
+
}
|
|
538
|
+
const nextNode = { ...node, id: nextId, type: 'block' };
|
|
539
|
+
if (nextId !== id) {
|
|
540
|
+
for (const candidate of Object.values(nodes)) {
|
|
541
|
+
if (!candidate || typeof candidate !== 'object')
|
|
542
|
+
continue;
|
|
543
|
+
if (candidate.parentId === id)
|
|
544
|
+
candidate.parentId = nextId;
|
|
545
|
+
if (Array.isArray(candidate.children)) {
|
|
546
|
+
candidate.children = candidate.children.map((childId) => childId === id ? nextId : childId);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return [nextId, nextNode];
|
|
551
|
+
}
|
|
552
|
+
function migrateBlockHostedItem(node) {
|
|
553
|
+
if (node.type !== 'item' ||
|
|
554
|
+
node.blockFaceId !== undefined ||
|
|
555
|
+
node.customMeshFaceId === undefined) {
|
|
556
|
+
return node;
|
|
557
|
+
}
|
|
558
|
+
const { customMeshFaceId, ...item } = node;
|
|
559
|
+
return { ...item, blockFaceId: customMeshFaceId };
|
|
560
|
+
}
|
|
485
561
|
function migrateNodes(nodes) {
|
|
486
562
|
// Repair pre-existing corruption (null children, zero-length walls) before
|
|
487
563
|
// any per-type migration runs, so already-saved scenes load cleanly.
|
|
@@ -490,6 +566,13 @@ function migrateNodes(nodes) {
|
|
|
490
566
|
// Scene materials minted while moving legacy wall fields onto `node.slots`;
|
|
491
567
|
// merged into the scene material map by the caller (`setScene`).
|
|
492
568
|
const mintedMaterials = {};
|
|
569
|
+
for (const [id, node] of Object.entries(patchedNodes)) {
|
|
570
|
+
const [nextId, nextNode] = migrateBlockRename(id, node, patchedNodes);
|
|
571
|
+
if (nextId !== id) {
|
|
572
|
+
delete patchedNodes[id];
|
|
573
|
+
}
|
|
574
|
+
patchedNodes[nextId] = migrateBlockHostedItem(nextNode);
|
|
575
|
+
}
|
|
493
576
|
// Pass 1: all node types except elevator.
|
|
494
577
|
// Elevator migration (migrateElevatorParent) mutates level.children to remove
|
|
495
578
|
// the elevator ID. If the elevator is processed before its parent level in
|
|
@@ -578,6 +661,34 @@ function migrateNodes(nodes) {
|
|
|
578
661
|
patchedNodes[id] = normalized;
|
|
579
662
|
}
|
|
580
663
|
}
|
|
664
|
+
// Dormers originally rendered one inline parametric window. Promote that
|
|
665
|
+
// default to a real hosted WindowNode so additional windows can use the
|
|
666
|
+
// regular window tool and inspector without changing the old appearance.
|
|
667
|
+
if (node.type === 'dormer') {
|
|
668
|
+
const hasLegacyInlineWindow = !Array.isArray(patchedNodes[id].children);
|
|
669
|
+
if (!hasLegacyInlineWindow)
|
|
670
|
+
continue;
|
|
671
|
+
const dormer = DormerNodeSchema.parse({
|
|
672
|
+
...patchedNodes[id],
|
|
673
|
+
children: getStringArray(patchedNodes[id].children),
|
|
674
|
+
});
|
|
675
|
+
const children = getStringArray(dormer.children);
|
|
676
|
+
const hasHostedWindow = children.some((childId) => patchedNodes[childId]?.type === 'window');
|
|
677
|
+
if (!hasHostedWindow) {
|
|
678
|
+
const baseWindowId = `window_${id.replace(/^dormer_/, '')}_default`;
|
|
679
|
+
let windowId = baseWindowId;
|
|
680
|
+
let suffix = 1;
|
|
681
|
+
while (patchedNodes[windowId]) {
|
|
682
|
+
windowId = `${baseWindowId}_${suffix}`;
|
|
683
|
+
suffix += 1;
|
|
684
|
+
}
|
|
685
|
+
const host = dormer.roofSegmentId ? patchedNodes[dormer.roofSegmentId] : undefined;
|
|
686
|
+
const hostSegment = host?.type === 'roof-segment' ? host : undefined;
|
|
687
|
+
const window = createDormerDefaultWindow(dormer, windowId, getDormerDefaultWindowFace(dormer, hostSegment));
|
|
688
|
+
patchedNodes[windowId] = window;
|
|
689
|
+
patchedNodes[id] = { ...dormer, children: [...children, window.id] };
|
|
690
|
+
}
|
|
691
|
+
}
|
|
581
692
|
if (node.type === 'construction-dimension') {
|
|
582
693
|
patchedNodes[id] = migrateConstructionDimension(node);
|
|
583
694
|
}
|
|
@@ -635,6 +746,26 @@ function migrateNodes(nodes) {
|
|
|
635
746
|
if (node.type === 'column') {
|
|
636
747
|
patchedNodes[id] = migrateSingleMaterialSlots(patchedNodes[id], ['shaft', 'base', 'capital', 'frame'], mintedMaterials);
|
|
637
748
|
}
|
|
749
|
+
if (node.type === 'gutter') {
|
|
750
|
+
patchedNodes[id] = migrateRenamedSlot(patchedNodes[id], 'surface', 'gutter');
|
|
751
|
+
patchedNodes[id] = migrateSingleMaterialSlots(patchedNodes[id], ['gutter'], mintedMaterials);
|
|
752
|
+
}
|
|
753
|
+
if (node.type === 'downspout') {
|
|
754
|
+
patchedNodes[id] = migrateSingleMaterialSlots(patchedNodes[id], ['surface'], mintedMaterials);
|
|
755
|
+
}
|
|
756
|
+
if (node.type === 'box-vent') {
|
|
757
|
+
patchedNodes[id] = migrateRoleMaterialSlots(patchedNodes[id], ['base', 'top'], mintedMaterials);
|
|
758
|
+
}
|
|
759
|
+
if (node.type === 'cupola') {
|
|
760
|
+
patchedNodes[id] = migrateCupolaLouverSlot(patchedNodes[id]);
|
|
761
|
+
patchedNodes[id] = migrateRoleMaterialSlots(patchedNodes[id], ['base', 'body', 'roof', 'louvers'], mintedMaterials);
|
|
762
|
+
}
|
|
763
|
+
if (node.type === 'eyebrow-vent') {
|
|
764
|
+
patchedNodes[id] = migrateRoleMaterialSlots(patchedNodes[id], ['hood', 'front'], mintedMaterials);
|
|
765
|
+
}
|
|
766
|
+
if (node.type === 'turbine-vent') {
|
|
767
|
+
patchedNodes[id] = migrateRoleMaterialSlots(patchedNodes[id], ['base', 'head'], mintedMaterials);
|
|
768
|
+
}
|
|
638
769
|
if (node.type === 'shelf') {
|
|
639
770
|
const normalized = normalizeShelfNode(node);
|
|
640
771
|
if (normalized) {
|
|
@@ -885,13 +1016,99 @@ function sceneHistorySnapshotFromState(state) {
|
|
|
885
1016
|
installedPlugins,
|
|
886
1017
|
};
|
|
887
1018
|
}
|
|
888
|
-
|
|
1019
|
+
/**
|
|
1020
|
+
* A dirty mark is a promise that some system will rebuild the node and clear
|
|
1021
|
+
* the mark, so marks are only accepted for kinds with a dirty consumer: kinds
|
|
1022
|
+
* with `dirtyTracking: false` (and kinds of disabled plugins) have none, and
|
|
1023
|
+
* a mark for them would sit in the set for the whole session and defeat every
|
|
1024
|
+
* consumer's empty-set early exit. Ids without a node pass: tools mark nodes
|
|
1025
|
+
* they are about to create.
|
|
1026
|
+
*/
|
|
1027
|
+
function isDirtyTrackable(id, scene) {
|
|
1028
|
+
const node = scene.nodes[id];
|
|
1029
|
+
if (!node)
|
|
1030
|
+
return true;
|
|
1031
|
+
if (!isNodeKindEnabled(node.type, scene.installedPlugins))
|
|
1032
|
+
return false;
|
|
1033
|
+
return nodeRegistry.get(node.type)?.dirtyTracking !== false;
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* `markDirty` always applied the consumer-kind guard, but many call sites add
|
|
1037
|
+
* to the raw set directly (that is how stuck `level` marks got in) — enforcing
|
|
1038
|
+
* it in `add` itself keeps them all honest.
|
|
1039
|
+
*/
|
|
1040
|
+
class GuardedDirtySet extends Set {
|
|
1041
|
+
getScene;
|
|
1042
|
+
constructor(getScene, from) {
|
|
1043
|
+
super();
|
|
1044
|
+
this.getScene = getScene;
|
|
1045
|
+
if (from)
|
|
1046
|
+
for (const id of from)
|
|
1047
|
+
this.add(id);
|
|
1048
|
+
}
|
|
1049
|
+
add(id) {
|
|
1050
|
+
if (!isDirtyTrackable(id, this.getScene()))
|
|
1051
|
+
return this;
|
|
1052
|
+
return super.add(id);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function createSceneStore(config) {
|
|
1056
|
+
const hydratedConfig = (set, get, store) => {
|
|
1057
|
+
const setWithHydration = (partial, replace) => {
|
|
1058
|
+
const state = get();
|
|
1059
|
+
let next = typeof partial === 'function' ? partial(state) : partial;
|
|
1060
|
+
const documentChanged = ['nodes', 'rootNodeIds', 'materials', 'collections', 'installedPlugins'].some((key) => (replace || key in next) && next[key] !== state[key]);
|
|
1061
|
+
if (documentChanged && !isHydrationNormalization()) {
|
|
1062
|
+
invalidatePendingHydration();
|
|
1063
|
+
if (state.hydrationToken)
|
|
1064
|
+
next = { ...next, hydrationToken: null };
|
|
1065
|
+
}
|
|
1066
|
+
if (replace)
|
|
1067
|
+
set(next, true);
|
|
1068
|
+
else
|
|
1069
|
+
set(next);
|
|
1070
|
+
};
|
|
1071
|
+
store.setState = setWithHydration;
|
|
1072
|
+
return config(setWithHydration, get, store);
|
|
1073
|
+
};
|
|
1074
|
+
return create()(hydratedConfig);
|
|
1075
|
+
}
|
|
1076
|
+
function runTemporalJump(target, jump) {
|
|
1077
|
+
if (!target?.nodes) {
|
|
1078
|
+
jump();
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
const before = useScene.getState().nodes;
|
|
1082
|
+
const changed = new Set();
|
|
1083
|
+
for (const id of new Set([...Object.keys(before), ...Object.keys(target.nodes)])) {
|
|
1084
|
+
const nodeId = id;
|
|
1085
|
+
const previous = before[nodeId];
|
|
1086
|
+
const next = target.nodes[nodeId];
|
|
1087
|
+
if (previous === next)
|
|
1088
|
+
continue;
|
|
1089
|
+
// Structural hierarchy changes keep the full-level reconciliation fallback.
|
|
1090
|
+
if ([previous, next].some((node) => node && ['site', 'building', 'level'].includes(node.type))) {
|
|
1091
|
+
jump();
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
changed.add(nodeId);
|
|
1095
|
+
}
|
|
1096
|
+
runWithSceneCommitNodeIds(changed, jump);
|
|
1097
|
+
}
|
|
1098
|
+
const useScene = createSceneStore(temporal((set, get) => ({
|
|
889
1099
|
// 1. Flat dictionary of all nodes
|
|
890
1100
|
nodes: {},
|
|
891
1101
|
// 2. Root node IDs
|
|
892
1102
|
rootNodeIds: [],
|
|
893
1103
|
// 3. Dirty set
|
|
894
|
-
dirtyNodes: new
|
|
1104
|
+
dirtyNodes: new GuardedDirtySet(get),
|
|
1105
|
+
hydrationToken: null,
|
|
1106
|
+
hydrationId: null,
|
|
1107
|
+
invalidateHydration: () => {
|
|
1108
|
+
invalidatePendingHydration();
|
|
1109
|
+
if (get().hydrationToken)
|
|
1110
|
+
set({ hydrationToken: null });
|
|
1111
|
+
},
|
|
895
1112
|
// 4. Collections
|
|
896
1113
|
collections: {},
|
|
897
1114
|
materials: {},
|
|
@@ -901,10 +1118,13 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
901
1118
|
readOnly: false,
|
|
902
1119
|
setReadOnly: (readOnly) => set({ readOnly }),
|
|
903
1120
|
unloadScene: () => {
|
|
1121
|
+
invalidatePendingHydration();
|
|
904
1122
|
set({
|
|
1123
|
+
hydrationToken: null,
|
|
1124
|
+
hydrationId: null,
|
|
905
1125
|
nodes: {},
|
|
906
1126
|
rootNodeIds: [],
|
|
907
|
-
dirtyNodes: new
|
|
1127
|
+
dirtyNodes: new GuardedDirtySet(get),
|
|
908
1128
|
collections: {},
|
|
909
1129
|
materials: {},
|
|
910
1130
|
installedPlugins: [],
|
|
@@ -947,26 +1167,65 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
947
1167
|
// pre-write state onto `pastStates`. Writing the scene in two steps
|
|
948
1168
|
// (as this used to) exposed a half-normalized intermediate state —
|
|
949
1169
|
// and the pre-load (possibly empty) state — as undo targets.
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1170
|
+
const hydrationId = {};
|
|
1171
|
+
runSceneHydration(() => {
|
|
1172
|
+
set({
|
|
1173
|
+
hydrationToken: null,
|
|
1174
|
+
hydrationId,
|
|
1175
|
+
nodes: cleanedNodes,
|
|
1176
|
+
rootNodeIds: normalizedRootNodeIds,
|
|
1177
|
+
dirtyNodes: new GuardedDirtySet(get),
|
|
1178
|
+
collections: extra?.collections ?? {},
|
|
1179
|
+
materials,
|
|
1180
|
+
installedPlugins: Array.from(new Set(extra?.installedPlugins ?? [])),
|
|
1181
|
+
hasExplicitPluginInstallState: extra?.hasExplicitPluginInstallState ?? false,
|
|
1182
|
+
});
|
|
1183
|
+
const applyNormalization = (updates) => {
|
|
1184
|
+
if (updates.length > 0)
|
|
1185
|
+
get().updateNodes(updates);
|
|
1186
|
+
};
|
|
1187
|
+
const hydratedNodes = Object.values(get().nodes);
|
|
1188
|
+
if (!get().readOnly) {
|
|
1189
|
+
pauseSceneHistory(useScene);
|
|
1190
|
+
try {
|
|
1191
|
+
if (hydratedNodes.some((node) => node.type === 'elevator')) {
|
|
1192
|
+
applyNormalization(syncAutoElevatorOpenings(get().nodes));
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
finally {
|
|
1196
|
+
resumeSceneHistory(useScene);
|
|
1197
|
+
}
|
|
1198
|
+
if (hydratedNodes.some((node) => node.type === 'stair')) {
|
|
1199
|
+
// Spatial-grid subscribers must settle first. Owning this pass
|
|
1200
|
+
// here also covers opening systems that mount after the load.
|
|
1201
|
+
queueSceneNormalization(() => {
|
|
1202
|
+
if (get().hydrationId !== hydrationId)
|
|
1203
|
+
return;
|
|
1204
|
+
pauseSceneHistory(useScene);
|
|
1205
|
+
try {
|
|
1206
|
+
applyNormalization(syncStairRises(get().nodes));
|
|
1207
|
+
applyNormalization(syncAutoStairOpenings(get().nodes));
|
|
1208
|
+
}
|
|
1209
|
+
finally {
|
|
1210
|
+
resumeSceneHistory(useScene);
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
// Mark all nodes as dirty to trigger re-validation
|
|
1216
|
+
Object.values(get().nodes).forEach((node) => {
|
|
1217
|
+
get().markDirty(node.id);
|
|
1218
|
+
});
|
|
1219
|
+
}, () => set({ hydrationToken: hydrationId }));
|
|
963
1220
|
},
|
|
964
1221
|
setInstalledPlugins: (pluginIds, options) => {
|
|
965
1222
|
if (get().readOnly)
|
|
966
1223
|
return;
|
|
967
1224
|
const nextInstalledPlugins = Array.from(new Set(pluginIds));
|
|
968
1225
|
const previousInstalledPlugins = get().installedPlugins;
|
|
969
|
-
|
|
1226
|
+
// Guard against the *next* plugin list: the store still holds the old
|
|
1227
|
+
// one, and re-marks for newly enabled kinds must pass the guard.
|
|
1228
|
+
const dirtyNodes = new GuardedDirtySet(() => ({ nodes: get().nodes, installedPlugins: nextInstalledPlugins }), get().dirtyNodes);
|
|
970
1229
|
for (const node of Object.values(get().nodes)) {
|
|
971
1230
|
if (!getNodePluginId(node.type))
|
|
972
1231
|
continue;
|
|
@@ -1017,10 +1276,9 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
1017
1276
|
set({ nodes, rootNodeIds });
|
|
1018
1277
|
},
|
|
1019
1278
|
markDirty: (id) => {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
if (node && nodeRegistry.get(node.type)?.dirtyTracking === false)
|
|
1279
|
+
// Guarded here too, not just in GuardedDirtySet.add — tests (and any
|
|
1280
|
+
// setState caller) can swap in a plain Set.
|
|
1281
|
+
if (!isDirtyTrackable(id, get()))
|
|
1024
1282
|
return;
|
|
1025
1283
|
get().dirtyNodes.add(id);
|
|
1026
1284
|
},
|
|
@@ -1170,8 +1428,27 @@ const useScene = create()(temporal((set, get) => ({
|
|
|
1170
1428
|
current: sceneHistorySnapshotFromState(currentState),
|
|
1171
1429
|
});
|
|
1172
1430
|
},
|
|
1431
|
+
wrapTemporal: (config) => (set, get, store) => {
|
|
1432
|
+
const state = config(set, get, store);
|
|
1433
|
+
return {
|
|
1434
|
+
...state,
|
|
1435
|
+
undo: (steps = 1) => runTemporalJump(get().pastStates.slice().splice(-steps, steps)[0], () => state.undo(steps)),
|
|
1436
|
+
redo: (steps = 1) => runTemporalJump(get().futureStates.slice().splice(-steps, steps)[0], () => state.redo(steps)),
|
|
1437
|
+
};
|
|
1438
|
+
},
|
|
1173
1439
|
limit: 50, // Limit to last 50 actions
|
|
1174
1440
|
}));
|
|
1441
|
+
// Live state belongs to the hydration owner so even a lazy consumer cannot
|
|
1442
|
+
// miss an override that was set and cleared before its first frame.
|
|
1443
|
+
const invalidateForLiveState = () => {
|
|
1444
|
+
if (useLiveNodeOverrides.getState().overrides.size ||
|
|
1445
|
+
useLiveTransforms.getState().transforms.size) {
|
|
1446
|
+
useScene.getState().invalidateHydration();
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
useLiveNodeOverrides.subscribe(invalidateForLiveState);
|
|
1450
|
+
useLiveTransforms.subscribe(invalidateForLiveState);
|
|
1451
|
+
useScene.subscribe(invalidateForLiveState);
|
|
1175
1452
|
export default useScene;
|
|
1176
1453
|
let sceneReadOnlyLeaseCount = 0;
|
|
1177
1454
|
let sceneReadOnlyLeaseBaseline = false;
|
|
@@ -1543,6 +1820,8 @@ export function applySceneSnapshot(snapshot, options) {
|
|
|
1543
1820
|
if (!temporalState.isTracking || getSceneHistoryPauseDepth() > 0) {
|
|
1544
1821
|
throw new Error('Cannot replace the scene snapshot during an active interaction');
|
|
1545
1822
|
}
|
|
1823
|
+
useLiveNodeOverrides.getState().clearAll();
|
|
1824
|
+
useLiveTransforms.getState().clearAll();
|
|
1546
1825
|
pauseSceneHistory(useScene);
|
|
1547
1826
|
try {
|
|
1548
1827
|
useScene.getState().setScene(snapshot.nodes, snapshot.rootNodeIds, {
|
|
@@ -1555,18 +1834,15 @@ export function applySceneSnapshot(snapshot, options) {
|
|
|
1555
1834
|
finally {
|
|
1556
1835
|
resumeSceneHistory(useScene);
|
|
1557
1836
|
}
|
|
1558
|
-
useLiveNodeOverrides.getState().clearAll();
|
|
1559
|
-
useLiveTransforms.getState().clearAll();
|
|
1560
1837
|
const current = sceneHistorySnapshotFromState(useScene.getState());
|
|
1561
1838
|
if (areSceneSnapshotsEqual(before, current))
|
|
1562
1839
|
return false;
|
|
1563
1840
|
notifySceneCommit({ origin: options.origin, before, current });
|
|
1564
1841
|
return true;
|
|
1565
1842
|
}
|
|
1566
|
-
// Track previous temporal state lengths
|
|
1843
|
+
// Track previous temporal state lengths for identifying history jumps
|
|
1567
1844
|
let prevPastLength = 0;
|
|
1568
1845
|
let prevFutureLength = 0;
|
|
1569
|
-
let prevNodesSnapshot = null;
|
|
1570
1846
|
export function clearSceneHistory() {
|
|
1571
1847
|
resetSceneHistoryPauseDepth();
|
|
1572
1848
|
// Resetting the pause-depth counter without resuming would strand the
|
|
@@ -1578,10 +1854,14 @@ export function clearSceneHistory() {
|
|
|
1578
1854
|
useScene.temporal.getState().clear();
|
|
1579
1855
|
prevPastLength = 0;
|
|
1580
1856
|
prevFutureLength = 0;
|
|
1581
|
-
prevNodesSnapshot = null;
|
|
1582
1857
|
}
|
|
1583
1858
|
// Subscribe to the temporal store (Undo/Redo events)
|
|
1584
|
-
useScene.temporal.subscribe((state) => {
|
|
1859
|
+
useScene.temporal.subscribe((state, previousState) => {
|
|
1860
|
+
// Zundo mutates its source stack before writing the scene. Reconciliation's
|
|
1861
|
+
// pause/resume notifications must not advance our pre-jump stack lengths.
|
|
1862
|
+
if (state.pastStates === previousState.pastStates &&
|
|
1863
|
+
state.futureStates === previousState.futureStates)
|
|
1864
|
+
return;
|
|
1585
1865
|
const currentPastLength = state.pastStates.length;
|
|
1586
1866
|
const currentFutureLength = state.futureStates.length;
|
|
1587
1867
|
// Undo: futureStates increases (state moved from past to future)
|
|
@@ -1589,40 +1869,21 @@ useScene.temporal.subscribe((state) => {
|
|
|
1589
1869
|
const didUndo = currentFutureLength > prevFutureLength;
|
|
1590
1870
|
const didRedo = currentPastLength > prevPastLength && currentFutureLength < prevFutureLength;
|
|
1591
1871
|
if (didUndo || didRedo) {
|
|
1592
|
-
// Capture
|
|
1593
|
-
|
|
1872
|
+
// Capture both layouts before another synchronous jump can replace them.
|
|
1873
|
+
// The state pushed onto the opposite stack includes history-paused derived
|
|
1874
|
+
// writes (such as stair rise), unlike a snapshot saved at the last edit.
|
|
1875
|
+
const snapshotBefore = didUndo
|
|
1876
|
+
? state.futureStates[prevFutureLength]?.nodes
|
|
1877
|
+
: state.pastStates[prevPastLength]?.nodes;
|
|
1878
|
+
const snapshotAfter = useScene.getState().nodes;
|
|
1594
1879
|
// Defer to a microtask so the scene store has settled before we diff,
|
|
1595
1880
|
// but still mark walls/items dirty before the next paint.
|
|
1596
1881
|
queueMicrotask(() => {
|
|
1597
1882
|
const currentNodes = useScene.getState().nodes;
|
|
1598
1883
|
const { markDirty } = useScene.getState();
|
|
1599
1884
|
if (snapshotBefore) {
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
if (snapshotBefore[id] !== node) {
|
|
1603
|
-
markDirty(id);
|
|
1604
|
-
// Also mark parent so merged geometries update
|
|
1605
|
-
if (node.parentId)
|
|
1606
|
-
markDirty(node.parentId);
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
// Nodes that were deleted (exist in prev but not current)
|
|
1610
|
-
for (const [id, node] of Object.entries(snapshotBefore)) {
|
|
1611
|
-
if (!currentNodes[id]) {
|
|
1612
|
-
const parentId = node.parentId;
|
|
1613
|
-
if (parentId) {
|
|
1614
|
-
markDirty(parentId);
|
|
1615
|
-
// Mark sibling nodes dirty so they can update their geometry
|
|
1616
|
-
// (e.g. adjacent walls need to recalculate miter/junction geometry)
|
|
1617
|
-
const parent = currentNodes[parentId];
|
|
1618
|
-
if (parent && 'children' in parent && Array.isArray(parent.children)) {
|
|
1619
|
-
for (const childId of parent.children) {
|
|
1620
|
-
markDirty(childId);
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1885
|
+
for (const id of getHistoryDirtyNodeIds(snapshotBefore, snapshotAfter))
|
|
1886
|
+
markDirty(id);
|
|
1626
1887
|
}
|
|
1627
1888
|
else {
|
|
1628
1889
|
// No snapshot to diff against — fall back to marking all
|
|
@@ -1630,10 +1891,17 @@ useScene.temporal.subscribe((state) => {
|
|
|
1630
1891
|
markDirty(node.id);
|
|
1631
1892
|
}
|
|
1632
1893
|
}
|
|
1894
|
+
// Undo/redo rewrites `nodes` without going through the delete actions,
|
|
1895
|
+
// so marks for nodes that no longer exist would sit in the set for the
|
|
1896
|
+
// rest of the session — no system clears a mark whose node is gone.
|
|
1897
|
+
const { dirtyNodes, clearDirty } = useScene.getState();
|
|
1898
|
+
for (const id of [...dirtyNodes]) {
|
|
1899
|
+
if (!currentNodes[id])
|
|
1900
|
+
clearDirty(id);
|
|
1901
|
+
}
|
|
1633
1902
|
});
|
|
1634
1903
|
}
|
|
1635
|
-
// Update tracked lengths
|
|
1904
|
+
// Update tracked lengths
|
|
1636
1905
|
prevPastLength = currentPastLength;
|
|
1637
1906
|
prevFutureLength = currentFutureLength;
|
|
1638
|
-
prevNodesSnapshot = useScene.getState().nodes;
|
|
1639
1907
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roof-elevation-system.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-elevation-system.tsx"],"names":[],"mappings":"AAgCA,wBAAgB,2BAA2B,IAAI,MAAM,IAAI,CAmExD;AAED,wBAAgB,mBAAmB,SAGlC"}
|