@pascal-app/core 0.7.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/events/bus.d.ts +47 -18
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/bus.js +1 -1
- package/dist/hooks/scene-registry/scene-registry.d.ts +6 -22
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.js +33 -26
- package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +14 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.js +51 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +2 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +273 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +13 -2
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +95 -9
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +28 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +91 -18
- package/dist/index.d.ts +22 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -2
- package/dist/lib/polygon-geometry.d.ts.map +1 -1
- package/dist/lib/polygon-relations.d.ts +10 -0
- package/dist/lib/polygon-relations.d.ts.map +1 -0
- package/dist/lib/polygon-relations.js +73 -0
- package/dist/lib/space-detection-pause.test.d.ts +2 -0
- package/dist/lib/space-detection-pause.test.d.ts.map +1 -0
- package/dist/lib/space-detection-pause.test.js +42 -0
- package/dist/lib/space-detection.d.ts +30 -1
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +222 -41
- package/dist/lib/space-detection.test.d.ts +2 -0
- package/dist/lib/space-detection.test.d.ts.map +1 -0
- package/dist/lib/space-detection.test.js +77 -0
- package/dist/material-library.d.ts +1 -1
- package/dist/material-library.d.ts.map +1 -1
- package/dist/material-library.js +1957 -141
- package/dist/registry/__bench__/relations-resolver.bench.d.ts +15 -0
- package/dist/registry/__bench__/relations-resolver.bench.d.ts.map +1 -0
- package/dist/registry/__bench__/relations-resolver.bench.js +170 -0
- package/dist/registry/handles.d.ts +321 -0
- package/dist/registry/handles.d.ts.map +1 -0
- package/dist/registry/handles.js +24 -0
- package/dist/registry/index.d.ts +7 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +4 -0
- package/dist/registry/registry.d.ts +93 -0
- package/dist/registry/registry.d.ts.map +1 -0
- package/dist/registry/registry.js +201 -0
- package/dist/registry/registry.test.d.ts +2 -0
- package/dist/registry/registry.test.d.ts.map +1 -0
- package/dist/registry/registry.test.js +183 -0
- package/dist/registry/relations-resolver.d.ts +42 -0
- package/dist/registry/relations-resolver.d.ts.map +1 -0
- package/dist/registry/relations-resolver.js +77 -0
- package/dist/registry/relations-resolver.test.d.ts +2 -0
- package/dist/registry/relations-resolver.test.d.ts.map +1 -0
- package/dist/registry/relations-resolver.test.js +183 -0
- package/dist/registry/scene-api.d.ts +44 -0
- package/dist/registry/scene-api.d.ts.map +1 -0
- package/dist/registry/scene-api.js +111 -0
- package/dist/registry/scene-api.test.d.ts +2 -0
- package/dist/registry/scene-api.test.d.ts.map +1 -0
- package/dist/registry/scene-api.test.js +183 -0
- package/dist/registry/subtree.d.ts +67 -0
- package/dist/registry/subtree.d.ts.map +1 -0
- package/dist/registry/subtree.js +119 -0
- package/dist/registry/subtree.test.d.ts +2 -0
- package/dist/registry/subtree.test.d.ts.map +1 -0
- package/dist/registry/subtree.test.js +133 -0
- package/dist/registry/types.d.ts +1420 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/types.js +1 -0
- package/dist/schema/asset-url.test.js +0 -4
- package/dist/schema/index.d.ts +17 -2
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +17 -3
- package/dist/schema/material.d.ts +12 -2
- package/dist/schema/material.d.ts.map +1 -1
- package/dist/schema/material.js +10 -0
- package/dist/schema/nodes/box-vent.d.ts +74 -0
- package/dist/schema/nodes/box-vent.d.ts.map +1 -0
- package/dist/schema/nodes/box-vent.js +63 -0
- package/dist/schema/nodes/building.d.ts +1 -1
- package/dist/schema/nodes/building.d.ts.map +1 -1
- package/dist/schema/nodes/building.js +3 -2
- package/dist/schema/nodes/ceiling.d.ts +3 -1
- package/dist/schema/nodes/ceiling.d.ts.map +1 -1
- package/dist/schema/nodes/ceiling.js +1 -1
- package/dist/schema/nodes/chimney.d.ts +158 -0
- package/dist/schema/nodes/chimney.d.ts.map +1 -0
- package/dist/schema/nodes/chimney.js +67 -0
- package/dist/schema/nodes/column.d.ts +598 -6
- package/dist/schema/nodes/column.d.ts.map +1 -1
- package/dist/schema/nodes/column.js +586 -6
- package/dist/schema/nodes/cupola.d.ts +66 -0
- package/dist/schema/nodes/cupola.d.ts.map +1 -0
- package/dist/schema/nodes/cupola.js +28 -0
- package/dist/schema/nodes/door.d.ts +1 -1
- package/dist/schema/nodes/dormer.d.ts +228 -0
- package/dist/schema/nodes/dormer.d.ts.map +1 -0
- package/dist/schema/nodes/dormer.js +119 -0
- package/dist/schema/nodes/downspout.d.ts +75 -0
- package/dist/schema/nodes/downspout.d.ts.map +1 -0
- package/dist/schema/nodes/downspout.js +63 -0
- package/dist/schema/nodes/elevator.d.ts +105 -0
- package/dist/schema/nodes/elevator.d.ts.map +1 -0
- package/dist/schema/nodes/elevator.js +54 -0
- package/dist/schema/nodes/eyebrow-vent.d.ts +68 -0
- package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -0
- package/dist/schema/nodes/eyebrow-vent.js +40 -0
- package/dist/schema/nodes/fence.d.ts +3 -2
- package/dist/schema/nodes/fence.d.ts.map +1 -1
- package/dist/schema/nodes/fence.js +4 -2
- package/dist/schema/nodes/gutter.d.ts +84 -0
- package/dist/schema/nodes/gutter.d.ts.map +1 -0
- package/dist/schema/nodes/gutter.js +76 -0
- package/dist/schema/nodes/item.d.ts +16 -0
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +21 -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/ridge-vent.d.ts +67 -0
- package/dist/schema/nodes/ridge-vent.d.ts.map +1 -0
- package/dist/schema/nodes/ridge-vent.js +30 -0
- package/dist/schema/nodes/roof-segment.d.ts +198 -4
- package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment.js +254 -3
- package/dist/schema/nodes/roof.d.ts +6 -6
- package/dist/schema/nodes/roof.d.ts.map +1 -1
- package/dist/schema/nodes/roof.js +9 -21
- package/dist/schema/nodes/shelf.d.ts +105 -0
- package/dist/schema/nodes/shelf.d.ts.map +1 -0
- package/dist/schema/nodes/shelf.js +84 -0
- package/dist/schema/nodes/site.d.ts +1 -116
- package/dist/schema/nodes/site.d.ts.map +1 -1
- package/dist/schema/nodes/site.js +2 -6
- package/dist/schema/nodes/skylight.d.ts +279 -0
- package/dist/schema/nodes/skylight.d.ts.map +1 -0
- package/dist/schema/nodes/skylight.js +152 -0
- package/dist/schema/nodes/slab.d.ts +3 -1
- package/dist/schema/nodes/slab.d.ts.map +1 -1
- package/dist/schema/nodes/slab.js +1 -1
- package/dist/schema/nodes/solar-panel.d.ts +117 -0
- package/dist/schema/nodes/solar-panel.d.ts.map +1 -0
- package/dist/schema/nodes/solar-panel.js +47 -0
- package/dist/schema/nodes/stair-segment.d.ts +1 -1
- package/dist/schema/nodes/stair.d.ts +6 -6
- package/dist/schema/nodes/stair.d.ts.map +1 -1
- package/dist/schema/nodes/stair.js +9 -7
- package/dist/schema/nodes/surface-hole-metadata.d.ts +2 -0
- package/dist/schema/nodes/surface-hole-metadata.d.ts.map +1 -1
- package/dist/schema/nodes/surface-hole-metadata.js +3 -1
- package/dist/schema/nodes/turbine-vent.d.ts +68 -0
- package/dist/schema/nodes/turbine-vent.d.ts.map +1 -0
- package/dist/schema/nodes/turbine-vent.js +41 -0
- package/dist/schema/nodes/wall.d.ts +3 -3
- package/dist/schema/nodes/window.d.ts +1 -1
- package/dist/schema/types.d.ts +1448 -236
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +26 -0
- package/dist/services/alignment-anchors.d.ts +100 -0
- package/dist/services/alignment-anchors.d.ts.map +1 -0
- package/dist/services/alignment-anchors.js +283 -0
- package/dist/services/alignment-anchors.test.d.ts +2 -0
- package/dist/services/alignment-anchors.test.d.ts.map +1 -0
- package/dist/services/alignment-anchors.test.js +383 -0
- package/dist/services/alignment.d.ts +126 -0
- package/dist/services/alignment.d.ts.map +1 -0
- package/dist/services/alignment.js +169 -0
- package/dist/services/alignment.test.d.ts +2 -0
- package/dist/services/alignment.test.d.ts.map +1 -0
- package/dist/services/alignment.test.js +92 -0
- package/dist/services/drag-session.d.ts +47 -0
- package/dist/services/drag-session.d.ts.map +1 -0
- package/dist/services/drag-session.js +100 -0
- package/dist/services/drag-session.test.d.ts +2 -0
- package/dist/services/drag-session.test.d.ts.map +1 -0
- package/dist/services/drag-session.test.js +219 -0
- package/dist/services/hosting.d.ts +80 -0
- package/dist/services/hosting.d.ts.map +1 -0
- package/dist/services/hosting.js +122 -0
- package/dist/services/hosting.test.d.ts +2 -0
- package/dist/services/hosting.test.d.ts.map +1 -0
- package/dist/services/hosting.test.js +199 -0
- package/dist/services/index.d.ts +7 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +6 -0
- package/dist/services/movement.d.ts +48 -0
- package/dist/services/movement.d.ts.map +1 -0
- package/dist/services/movement.js +65 -0
- package/dist/services/movement.test.d.ts +2 -0
- package/dist/services/movement.test.d.ts.map +1 -0
- package/dist/services/movement.test.js +133 -0
- package/dist/services/single-undo-dance.test.d.ts +2 -0
- package/dist/services/single-undo-dance.test.d.ts.map +1 -0
- package/dist/services/single-undo-dance.test.js +210 -0
- package/dist/services/snap.d.ts +72 -0
- package/dist/services/snap.d.ts.map +1 -0
- package/dist/services/snap.js +120 -0
- package/dist/services/snap.test.d.ts +2 -0
- package/dist/services/snap.test.d.ts.map +1 -0
- package/dist/services/snap.test.js +95 -0
- package/dist/solar-panel-presets.d.ts +17 -0
- package/dist/solar-panel-presets.d.ts.map +1 -0
- package/dist/solar-panel-presets.js +43 -0
- package/dist/store/actions/node-actions.d.ts +14 -2
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +499 -20
- package/dist/store/actions/node-mutation-sanitize.test.d.ts +2 -0
- package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +1 -0
- package/dist/store/actions/node-mutation-sanitize.test.js +147 -0
- package/dist/store/actions/reparent.test.d.ts +2 -0
- package/dist/store/actions/reparent.test.d.ts.map +1 -0
- package/dist/store/actions/reparent.test.js +212 -0
- package/dist/store/use-interactive.d.ts +39 -0
- package/dist/store/use-interactive.d.ts.map +1 -1
- package/dist/store/use-interactive.js +75 -0
- package/dist/store/use-live-node-overrides.d.ts +22 -0
- package/dist/store/use-live-node-overrides.d.ts.map +1 -0
- package/dist/store/use-live-node-overrides.js +59 -0
- package/dist/store/use-scene-elevator-migration.test.d.ts +2 -0
- package/dist/store/use-scene-elevator-migration.test.d.ts.map +1 -0
- package/dist/store/use-scene-elevator-migration.test.js +109 -0
- package/dist/store/use-scene.d.ts +11 -0
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +207 -11
- package/dist/systems/elevator/elevator-dispatch.d.ts +12 -0
- package/dist/systems/elevator/elevator-dispatch.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-dispatch.js +57 -0
- package/dist/systems/elevator/elevator-geometry.d.ts +15 -0
- package/dist/systems/elevator/elevator-geometry.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-geometry.js +50 -0
- package/dist/systems/elevator/elevator-opening-sync.d.ts +6 -0
- package/dist/systems/elevator/elevator-opening-sync.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-opening-sync.js +218 -0
- package/dist/systems/elevator/elevator-opening-sync.test.d.ts +2 -0
- package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-opening-sync.test.js +146 -0
- package/dist/systems/elevator/elevator-opening-system.d.ts +2 -0
- package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-opening-system.js +48 -0
- package/dist/systems/elevator/elevator-runtime-system.d.ts +2 -0
- package/dist/systems/elevator/elevator-runtime-system.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-runtime-system.js +8 -0
- package/dist/systems/elevator/elevator-runtime.d.ts +18 -0
- package/dist/systems/elevator/elevator-runtime.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-runtime.js +225 -0
- package/dist/systems/elevator/elevator-runtime.test.d.ts +2 -0
- package/dist/systems/elevator/elevator-runtime.test.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-runtime.test.js +64 -0
- package/dist/systems/elevator/elevator-service.d.ts +19 -0
- package/dist/systems/elevator/elevator-service.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-service.js +111 -0
- package/dist/systems/stair/stair-footprint.d.ts +44 -0
- package/dist/systems/stair/stair-footprint.d.ts.map +1 -0
- package/dist/systems/stair/stair-footprint.js +168 -0
- package/dist/systems/stair/stair-opening-preview.d.ts +1777 -0
- package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -0
- package/dist/systems/stair/stair-opening-preview.js +93 -0
- package/dist/systems/stair/stair-opening-preview.test.d.ts +2 -0
- package/dist/systems/stair/stair-opening-preview.test.d.ts.map +1 -0
- package/dist/systems/stair/stair-opening-preview.test.js +80 -0
- package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.js +183 -185
- package/dist/systems/stair/stair-opening-sync.test.js +385 -4
- package/dist/systems/stair/stair-opening-system.d.ts +2 -0
- package/dist/systems/stair/stair-opening-system.d.ts.map +1 -0
- package/dist/systems/stair/stair-opening-system.js +104 -0
- package/dist/systems/wall/wall-curve.d.ts +1 -1
- package/dist/systems/wall/wall-curve.d.ts.map +1 -1
- package/dist/systems/wall/wall-curve.js +1 -1
- package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
- package/dist/systems/wall/wall-mitering.js +2 -6
- package/dist/systems/wall/wall-move.d.ts +48 -0
- package/dist/systems/wall/wall-move.d.ts.map +1 -0
- package/dist/systems/wall/wall-move.js +193 -0
- package/dist/utils/clone-scene-graph.d.ts +6 -4
- package/dist/utils/clone-scene-graph.d.ts.map +1 -1
- package/dist/utils/clone-scene-graph.js +6 -4
- package/dist/utils/clone-scene-graph.test.d.ts +2 -0
- package/dist/utils/clone-scene-graph.test.d.ts.map +1 -0
- package/dist/utils/clone-scene-graph.test.js +59 -0
- package/dist/validation/validate-build-json.d.ts +43 -0
- package/dist/validation/validate-build-json.d.ts.map +1 -0
- package/dist/validation/validate-build-json.js +227 -0
- package/package.json +11 -3
- package/dist/materials.d.ts +0 -10
- package/dist/materials.d.ts.map +0 -1
- package/dist/materials.js +0 -22
- package/dist/systems/ceiling/ceiling-system.d.ts +0 -8
- package/dist/systems/ceiling/ceiling-system.d.ts.map +0 -1
- package/dist/systems/ceiling/ceiling-system.js +0 -92
- package/dist/systems/door/door-system.d.ts +0 -2
- package/dist/systems/door/door-system.d.ts.map +0 -1
- package/dist/systems/door/door-system.js +0 -195
- package/dist/systems/fence/fence-system.d.ts +0 -2
- package/dist/systems/fence/fence-system.d.ts.map +0 -1
- package/dist/systems/fence/fence-system.js +0 -187
- package/dist/systems/item/item-system.d.ts +0 -2
- package/dist/systems/item/item-system.d.ts.map +0 -1
- package/dist/systems/item/item-system.js +0 -48
- package/dist/systems/roof/roof-system.d.ts +0 -16
- package/dist/systems/roof/roof-system.d.ts.map +0 -1
- package/dist/systems/roof/roof-system.js +0 -797
- package/dist/systems/slab/slab-system.d.ts +0 -8
- package/dist/systems/slab/slab-system.d.ts.map +0 -1
- package/dist/systems/slab/slab-system.js +0 -214
- package/dist/systems/stair/stair-system.d.ts +0 -2
- package/dist/systems/stair/stair-system.d.ts.map +0 -1
- package/dist/systems/stair/stair-system.js +0 -776
- package/dist/systems/wall/wall-system.d.ts +0 -12
- package/dist/systems/wall/wall-system.d.ts.map +0 -1
- package/dist/systems/wall/wall-system.js +0 -455
- package/dist/systems/window/window-system.d.ts +0 -2
- package/dist/systems/window/window-system.d.ts.map +0 -1
- package/dist/systems/window/window-system.js +0 -131
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { useInteractive } from '../../store/use-interactive';
|
|
2
|
+
import useScene from '../../store/use-scene';
|
|
3
|
+
import { resolveElevatorLevels } from './elevator-service';
|
|
4
|
+
const EPSILON = 0.001;
|
|
5
|
+
function moveToward(current, target, maxDelta) {
|
|
6
|
+
const delta = target - current;
|
|
7
|
+
if (Math.abs(delta) <= maxDelta)
|
|
8
|
+
return target;
|
|
9
|
+
return current + Math.sign(delta) * maxDelta;
|
|
10
|
+
}
|
|
11
|
+
export function createElevatorInteractiveState(levelId, carY) {
|
|
12
|
+
return {
|
|
13
|
+
currentLevelId: levelId,
|
|
14
|
+
targetLevelId: null,
|
|
15
|
+
carY,
|
|
16
|
+
doorOpen: 0,
|
|
17
|
+
phase: 'idle',
|
|
18
|
+
phaseStartedAt: null,
|
|
19
|
+
queue: [],
|
|
20
|
+
requestedStops: [],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function getInitialElevatorState(elevatorId, nodes) {
|
|
24
|
+
const node = nodes[elevatorId];
|
|
25
|
+
if (node?.type !== 'elevator')
|
|
26
|
+
return null;
|
|
27
|
+
const { defaultEntry } = resolveElevatorLevels(node, nodes);
|
|
28
|
+
if (!defaultEntry)
|
|
29
|
+
return null;
|
|
30
|
+
return createElevatorInteractiveState(defaultEntry.id, defaultEntry.baseY);
|
|
31
|
+
}
|
|
32
|
+
function ensureElevatorState(elevatorId, nodes) {
|
|
33
|
+
const interactive = useInteractive.getState();
|
|
34
|
+
const existing = interactive.elevators[elevatorId];
|
|
35
|
+
if (existing)
|
|
36
|
+
return existing;
|
|
37
|
+
const initial = getInitialElevatorState(elevatorId, nodes);
|
|
38
|
+
if (!initial)
|
|
39
|
+
return null;
|
|
40
|
+
interactive.initElevator(elevatorId, initial.currentLevelId, initial.carY);
|
|
41
|
+
return initial;
|
|
42
|
+
}
|
|
43
|
+
export function queueElevatorRequest(state, levelId) {
|
|
44
|
+
const isAlreadyQueued = state.queue.includes(levelId) || state.targetLevelId === levelId;
|
|
45
|
+
if (isAlreadyQueued)
|
|
46
|
+
return state;
|
|
47
|
+
return {
|
|
48
|
+
...state,
|
|
49
|
+
queue: [...state.queue, levelId],
|
|
50
|
+
requestedStops: state.requestedStops.includes(levelId)
|
|
51
|
+
? state.requestedStops
|
|
52
|
+
: [...state.requestedStops, levelId],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function openElevatorDoorState(state) {
|
|
56
|
+
if (!state.currentLevelId || state.phase === 'moving')
|
|
57
|
+
return state;
|
|
58
|
+
return {
|
|
59
|
+
...state,
|
|
60
|
+
phase: 'opening',
|
|
61
|
+
phaseStartedAt: null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function requestElevatorLevel(elevatorId, levelId) {
|
|
65
|
+
const nodes = useScene.getState().nodes;
|
|
66
|
+
const current = ensureElevatorState(elevatorId, nodes);
|
|
67
|
+
if (!current)
|
|
68
|
+
return;
|
|
69
|
+
const next = queueElevatorRequest(current, levelId);
|
|
70
|
+
if (next === current)
|
|
71
|
+
return;
|
|
72
|
+
useInteractive.getState().setElevatorState(elevatorId, next);
|
|
73
|
+
}
|
|
74
|
+
export function openElevatorDoor(elevatorId) {
|
|
75
|
+
const nodes = useScene.getState().nodes;
|
|
76
|
+
const current = ensureElevatorState(elevatorId, nodes);
|
|
77
|
+
if (!current)
|
|
78
|
+
return;
|
|
79
|
+
const next = openElevatorDoorState(current);
|
|
80
|
+
if (next === current)
|
|
81
|
+
return;
|
|
82
|
+
useInteractive.getState().setElevatorState(elevatorId, next);
|
|
83
|
+
}
|
|
84
|
+
export function stepElevatorRuntimeState({ defaultEntry, delta, elevator, entries, now, state, }) {
|
|
85
|
+
const currentEntry = entries.find((entry) => entry.id === state.currentLevelId) ?? defaultEntry;
|
|
86
|
+
if (currentEntry.id !== state.currentLevelId) {
|
|
87
|
+
return {
|
|
88
|
+
...state,
|
|
89
|
+
currentLevelId: currentEntry.id,
|
|
90
|
+
carY: currentEntry.baseY,
|
|
91
|
+
targetLevelId: null,
|
|
92
|
+
phase: 'idle',
|
|
93
|
+
phaseStartedAt: null,
|
|
94
|
+
queue: [],
|
|
95
|
+
requestedStops: [],
|
|
96
|
+
doorOpen: 0,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const targetEntry = state.targetLevelId
|
|
100
|
+
? entries.find((entry) => entry.id === state.targetLevelId)
|
|
101
|
+
: state.queue[0]
|
|
102
|
+
? entries.find((entry) => entry.id === state.queue[0])
|
|
103
|
+
: null;
|
|
104
|
+
const doorDurationMs = Math.max(elevator.doorDurationMs ?? 900, 1);
|
|
105
|
+
const doorStep = (delta * 1000) / doorDurationMs;
|
|
106
|
+
switch (state.phase) {
|
|
107
|
+
case 'idle': {
|
|
108
|
+
const nextLevelId = state.queue[0] ?? null;
|
|
109
|
+
if (!nextLevelId) {
|
|
110
|
+
if (state.doorOpen > EPSILON) {
|
|
111
|
+
return {
|
|
112
|
+
...state,
|
|
113
|
+
doorOpen: Math.max(0, state.doorOpen - doorStep),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (state.requestedStops.length === 0)
|
|
117
|
+
return state;
|
|
118
|
+
return {
|
|
119
|
+
...state,
|
|
120
|
+
requestedStops: [],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
...state,
|
|
125
|
+
targetLevelId: nextLevelId,
|
|
126
|
+
phase: state.doorOpen > EPSILON
|
|
127
|
+
? 'closing'
|
|
128
|
+
: nextLevelId === state.currentLevelId
|
|
129
|
+
? 'opening'
|
|
130
|
+
: 'moving',
|
|
131
|
+
phaseStartedAt: now,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
case 'closing': {
|
|
135
|
+
const doorOpen = Math.max(0, state.doorOpen - doorStep);
|
|
136
|
+
return {
|
|
137
|
+
...state,
|
|
138
|
+
doorOpen,
|
|
139
|
+
phase: doorOpen <= EPSILON ? (state.targetLevelId ? 'moving' : 'idle') : 'closing',
|
|
140
|
+
phaseStartedAt: doorOpen <= EPSILON ? now : state.phaseStartedAt,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
case 'moving': {
|
|
144
|
+
if (!targetEntry) {
|
|
145
|
+
return {
|
|
146
|
+
...state,
|
|
147
|
+
targetLevelId: null,
|
|
148
|
+
phase: 'idle',
|
|
149
|
+
queue: [],
|
|
150
|
+
requestedStops: [],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const speed = Math.max(elevator.speed ?? 2.2, 0.1);
|
|
154
|
+
const nextY = moveToward(state.carY, targetEntry.baseY, speed * delta);
|
|
155
|
+
const arrived = Math.abs(nextY - targetEntry.baseY) <= EPSILON;
|
|
156
|
+
return {
|
|
157
|
+
...state,
|
|
158
|
+
carY: nextY,
|
|
159
|
+
currentLevelId: arrived ? targetEntry.id : state.currentLevelId,
|
|
160
|
+
phase: arrived ? 'opening' : 'moving',
|
|
161
|
+
phaseStartedAt: arrived ? now : state.phaseStartedAt,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
case 'opening': {
|
|
165
|
+
const doorOpen = Math.min(1, state.doorOpen + doorStep);
|
|
166
|
+
return {
|
|
167
|
+
...state,
|
|
168
|
+
doorOpen,
|
|
169
|
+
phase: doorOpen >= 1 - EPSILON ? 'open' : 'opening',
|
|
170
|
+
phaseStartedAt: doorOpen >= 1 - EPSILON ? now : state.phaseStartedAt,
|
|
171
|
+
targetLevelId: doorOpen >= 1 - EPSILON ? null : state.targetLevelId,
|
|
172
|
+
queue: doorOpen >= 1 - EPSILON && state.queue[0] === state.currentLevelId
|
|
173
|
+
? state.queue.slice(1)
|
|
174
|
+
: state.queue,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
case 'open': {
|
|
178
|
+
const elapsed = now - (state.phaseStartedAt ?? now);
|
|
179
|
+
if (elapsed < Math.max(elevator.dwellMs ?? 1400, 0))
|
|
180
|
+
return state;
|
|
181
|
+
return {
|
|
182
|
+
...state,
|
|
183
|
+
phase: 'closing',
|
|
184
|
+
phaseStartedAt: now,
|
|
185
|
+
targetLevelId: state.queue[0] ?? null,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export function stepElevatorRuntimes(now, delta) {
|
|
191
|
+
const nodes = useScene.getState().nodes;
|
|
192
|
+
const interactive = useInteractive.getState();
|
|
193
|
+
for (const elevatorId of Object.keys(interactive.elevators)) {
|
|
194
|
+
const typedElevatorId = elevatorId;
|
|
195
|
+
if (nodes[typedElevatorId]?.type !== 'elevator') {
|
|
196
|
+
interactive.removeElevator(typedElevatorId);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const node of Object.values(nodes)) {
|
|
200
|
+
if (node.type !== 'elevator')
|
|
201
|
+
continue;
|
|
202
|
+
const elevatorId = node.id;
|
|
203
|
+
const { entries, defaultEntry } = resolveElevatorLevels(node, nodes);
|
|
204
|
+
if (!defaultEntry)
|
|
205
|
+
continue;
|
|
206
|
+
const state = useInteractive.getState().elevators[elevatorId];
|
|
207
|
+
if (!state) {
|
|
208
|
+
useInteractive
|
|
209
|
+
.getState()
|
|
210
|
+
.initElevator(elevatorId, defaultEntry.id, defaultEntry.baseY);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const next = stepElevatorRuntimeState({
|
|
214
|
+
defaultEntry,
|
|
215
|
+
delta,
|
|
216
|
+
elevator: node,
|
|
217
|
+
entries,
|
|
218
|
+
now,
|
|
219
|
+
state,
|
|
220
|
+
});
|
|
221
|
+
if (next !== state) {
|
|
222
|
+
useInteractive.getState().setElevatorState(elevatorId, next);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"elevator-runtime.test.d.ts","sourceRoot":"","sources":["../../../src/systems/elevator/elevator-runtime.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { ElevatorNode } from '../../schema';
|
|
3
|
+
import { createElevatorInteractiveState, openElevatorDoorState, queueElevatorRequest, stepElevatorRuntimeState, } from './elevator-runtime';
|
|
4
|
+
const groundLevelId = 'level_ground';
|
|
5
|
+
const upperLevelId = 'level_upper';
|
|
6
|
+
const entries = [
|
|
7
|
+
{ id: groundLevelId, label: '0', baseY: 0 },
|
|
8
|
+
{ id: upperLevelId, label: '1', baseY: 2.5 },
|
|
9
|
+
];
|
|
10
|
+
const elevator = ElevatorNode.parse({
|
|
11
|
+
speed: 10,
|
|
12
|
+
doorDurationMs: 100,
|
|
13
|
+
dwellMs: 0,
|
|
14
|
+
});
|
|
15
|
+
describe('elevator runtime helpers', () => {
|
|
16
|
+
test('queues level requests without duplicating the target', () => {
|
|
17
|
+
const state = createElevatorInteractiveState(groundLevelId, 0);
|
|
18
|
+
const queued = queueElevatorRequest(state, upperLevelId);
|
|
19
|
+
const duplicated = queueElevatorRequest(queued, upperLevelId);
|
|
20
|
+
expect(queued.queue).toEqual([upperLevelId]);
|
|
21
|
+
expect(queued.requestedStops).toEqual([upperLevelId]);
|
|
22
|
+
expect(duplicated.queue).toEqual([upperLevelId]);
|
|
23
|
+
expect(duplicated.requestedStops).toEqual([upperLevelId]);
|
|
24
|
+
});
|
|
25
|
+
test('opens doors only when the elevator is not moving', () => {
|
|
26
|
+
const idle = createElevatorInteractiveState(groundLevelId, 0);
|
|
27
|
+
const moving = { ...idle, phase: 'moving' };
|
|
28
|
+
expect(openElevatorDoorState(idle).phase).toBe('opening');
|
|
29
|
+
expect(openElevatorDoorState(moving)).toBe(moving);
|
|
30
|
+
});
|
|
31
|
+
test('moves to a queued level and clears the served request on arrival', () => {
|
|
32
|
+
const queued = queueElevatorRequest(createElevatorInteractiveState(groundLevelId, 0), upperLevelId);
|
|
33
|
+
const moving = stepElevatorRuntimeState({
|
|
34
|
+
defaultEntry: entries[0],
|
|
35
|
+
delta: 0.016,
|
|
36
|
+
elevator,
|
|
37
|
+
entries,
|
|
38
|
+
now: 0,
|
|
39
|
+
state: queued,
|
|
40
|
+
});
|
|
41
|
+
const arrived = stepElevatorRuntimeState({
|
|
42
|
+
defaultEntry: entries[0],
|
|
43
|
+
delta: 1,
|
|
44
|
+
elevator,
|
|
45
|
+
entries,
|
|
46
|
+
now: 100,
|
|
47
|
+
state: moving,
|
|
48
|
+
});
|
|
49
|
+
const open = stepElevatorRuntimeState({
|
|
50
|
+
defaultEntry: entries[0],
|
|
51
|
+
delta: 1,
|
|
52
|
+
elevator,
|
|
53
|
+
entries,
|
|
54
|
+
now: 200,
|
|
55
|
+
state: arrived,
|
|
56
|
+
});
|
|
57
|
+
expect(moving.phase).toBe('moving');
|
|
58
|
+
expect(arrived.currentLevelId).toBe(upperLevelId);
|
|
59
|
+
expect(arrived.phase).toBe('opening');
|
|
60
|
+
expect(open.phase).toBe('open');
|
|
61
|
+
expect(open.queue).toEqual([]);
|
|
62
|
+
expect(open.requestedStops).toEqual([upperLevelId]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AnyNode, ElevatorNode, LevelNode } from '../../schema';
|
|
2
|
+
export declare const DEFAULT_ELEVATOR_LEVEL_HEIGHT = 2.5;
|
|
3
|
+
export type ElevatorLevelEntry = {
|
|
4
|
+
id: LevelNode['id'];
|
|
5
|
+
label: string;
|
|
6
|
+
baseY: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function resolveElevatorBuildingLevels(elevator: ElevatorNode, nodes: Record<string, AnyNode>): LevelNode[];
|
|
9
|
+
export declare function resolveElevatorServiceLevelIds(elevator: ElevatorNode, nodes: Record<string, AnyNode>): string[];
|
|
10
|
+
export declare function resolveElevatorServiceLevels(elevator: ElevatorNode, nodes: Record<string, AnyNode>): LevelNode[];
|
|
11
|
+
export declare function getElevatorLevelHeight(levelId: string, nodes: Record<string, AnyNode>): number;
|
|
12
|
+
export declare function resolveElevatorLevels(elevator: ElevatorNode, nodes: Record<string, AnyNode>): {
|
|
13
|
+
entries: ElevatorLevelEntry[];
|
|
14
|
+
defaultEntry: ElevatorLevelEntry | null;
|
|
15
|
+
shaftBaseY: number;
|
|
16
|
+
shaftTopY: number;
|
|
17
|
+
totalHeight: number;
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=elevator-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"elevator-service.d.ts","sourceRoot":"","sources":["../../../src/systems/elevator/elevator-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAGP,YAAY,EACZ,SAAS,EAEV,MAAM,cAAc,CAAA;AAErB,eAAO,MAAM,6BAA6B,MAAM,CAAA;AAEhD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,EAAE,SAAS,CAAC,IAAI,CAAC,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AA2BD,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,SAAS,EAAE,CAEb;AAED,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,MAAM,EAAE,CAEV;AAED,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,SAAS,EAAE,CAsBb;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAoB9F;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B;IACD,OAAO,EAAE,kBAAkB,EAAE,CAAA;IAC7B,YAAY,EAAE,kBAAkB,GAAG,IAAI,CAAA;IACvC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;CACpB,CA0CA"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export const DEFAULT_ELEVATOR_LEVEL_HEIGHT = 2.5;
|
|
2
|
+
function getBuildingLevels(elevator, nodes) {
|
|
3
|
+
const building = elevator.parentId && nodes[elevator.parentId]?.type === 'building'
|
|
4
|
+
? nodes[elevator.parentId]
|
|
5
|
+
: null;
|
|
6
|
+
if (building?.type !== 'building')
|
|
7
|
+
return [];
|
|
8
|
+
return building.children
|
|
9
|
+
.map((childId) => nodes[childId])
|
|
10
|
+
.filter((node) => node?.type === 'level')
|
|
11
|
+
.sort((left, right) => left.level - right.level);
|
|
12
|
+
}
|
|
13
|
+
function findLevelIndex(levels, levelId) {
|
|
14
|
+
if (!levelId)
|
|
15
|
+
return -1;
|
|
16
|
+
return levels.findIndex((level) => level.id === levelId);
|
|
17
|
+
}
|
|
18
|
+
function getDefaultToIndex(levels, fromIndex) {
|
|
19
|
+
if (levels.length === 0)
|
|
20
|
+
return -1;
|
|
21
|
+
if (fromIndex < 0)
|
|
22
|
+
return Math.min(1, levels.length - 1);
|
|
23
|
+
return Math.min(fromIndex + 1, levels.length - 1);
|
|
24
|
+
}
|
|
25
|
+
export function resolveElevatorBuildingLevels(elevator, nodes) {
|
|
26
|
+
return getBuildingLevels(elevator, nodes);
|
|
27
|
+
}
|
|
28
|
+
export function resolveElevatorServiceLevelIds(elevator, nodes) {
|
|
29
|
+
return resolveElevatorServiceLevels(elevator, nodes).map((level) => level.id);
|
|
30
|
+
}
|
|
31
|
+
export function resolveElevatorServiceLevels(elevator, nodes) {
|
|
32
|
+
const levels = getBuildingLevels(elevator, nodes);
|
|
33
|
+
if (levels.length === 0)
|
|
34
|
+
return [];
|
|
35
|
+
const hasServiceBounds = Boolean(elevator.fromLevelId || elevator.toLevelId);
|
|
36
|
+
let legacyServedLevels = [];
|
|
37
|
+
if (!hasServiceBounds && elevator.servedLevelIds && elevator.servedLevelIds.length > 0) {
|
|
38
|
+
const servedIds = new Set(elevator.servedLevelIds);
|
|
39
|
+
legacyServedLevels = levels.filter((level) => servedIds.has(level.id));
|
|
40
|
+
}
|
|
41
|
+
const legacyFromLevelId = legacyServedLevels[0]?.id ?? null;
|
|
42
|
+
const legacyToLevelId = legacyServedLevels[legacyServedLevels.length - 1]?.id ?? null;
|
|
43
|
+
const explicitFromIndex = findLevelIndex(levels, elevator.fromLevelId ?? legacyFromLevelId);
|
|
44
|
+
const defaultFromIndex = findLevelIndex(levels, elevator.defaultLevelId);
|
|
45
|
+
const fromIndex = explicitFromIndex >= 0 ? explicitFromIndex : Math.max(defaultFromIndex, 0);
|
|
46
|
+
const toIndex = findLevelIndex(levels, elevator.toLevelId ?? legacyToLevelId);
|
|
47
|
+
const resolvedToIndex = toIndex >= 0 ? toIndex : getDefaultToIndex(levels, fromIndex);
|
|
48
|
+
const minIndex = Math.min(fromIndex, resolvedToIndex);
|
|
49
|
+
const maxIndex = Math.max(fromIndex, resolvedToIndex);
|
|
50
|
+
return levels.slice(minIndex, maxIndex + 1);
|
|
51
|
+
}
|
|
52
|
+
export function getElevatorLevelHeight(levelId, nodes) {
|
|
53
|
+
const level = nodes[levelId];
|
|
54
|
+
if (!level || level.type !== 'level')
|
|
55
|
+
return DEFAULT_ELEVATOR_LEVEL_HEIGHT;
|
|
56
|
+
let maxTop = 0;
|
|
57
|
+
for (const childId of level.children) {
|
|
58
|
+
const child = nodes[childId];
|
|
59
|
+
if (!child)
|
|
60
|
+
continue;
|
|
61
|
+
if (child.type === 'ceiling') {
|
|
62
|
+
const height = child.height ?? DEFAULT_ELEVATOR_LEVEL_HEIGHT;
|
|
63
|
+
if (height > maxTop)
|
|
64
|
+
maxTop = height;
|
|
65
|
+
}
|
|
66
|
+
else if (child.type === 'wall') {
|
|
67
|
+
const height = child.height ?? DEFAULT_ELEVATOR_LEVEL_HEIGHT;
|
|
68
|
+
if (height > maxTop)
|
|
69
|
+
maxTop = height;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return maxTop > 0 ? maxTop : DEFAULT_ELEVATOR_LEVEL_HEIGHT;
|
|
73
|
+
}
|
|
74
|
+
export function resolveElevatorLevels(elevator, nodes) {
|
|
75
|
+
const allLevels = resolveElevatorBuildingLevels(elevator, nodes);
|
|
76
|
+
const baseYByLevelId = new Map();
|
|
77
|
+
let cumulativeY = 0;
|
|
78
|
+
for (const level of allLevels) {
|
|
79
|
+
baseYByLevelId.set(level.id, cumulativeY);
|
|
80
|
+
cumulativeY += getElevatorLevelHeight(level.id, nodes);
|
|
81
|
+
}
|
|
82
|
+
const serviceLevels = resolveElevatorServiceLevels(elevator, nodes);
|
|
83
|
+
const entries = serviceLevels.map((level) => ({
|
|
84
|
+
id: level.id,
|
|
85
|
+
label: String(level.level),
|
|
86
|
+
baseY: baseYByLevelId.get(level.id) ?? 0,
|
|
87
|
+
}));
|
|
88
|
+
const defaultEntry = entries.find((entry) => entry.id === elevator.defaultLevelId) ??
|
|
89
|
+
entries.find((entry) => entry.id === elevator.fromLevelId) ??
|
|
90
|
+
entries[0] ??
|
|
91
|
+
null;
|
|
92
|
+
const firstServedLevel = serviceLevels[0] ?? null;
|
|
93
|
+
const lastServedLevel = serviceLevels[serviceLevels.length - 1] ?? null;
|
|
94
|
+
const shaftBaseY = firstServedLevel ? (baseYByLevelId.get(firstServedLevel.id) ?? 0) : 0;
|
|
95
|
+
const lastServedIndex = lastServedLevel
|
|
96
|
+
? allLevels.findIndex((level) => level.id === lastServedLevel.id)
|
|
97
|
+
: -1;
|
|
98
|
+
const nextLevel = lastServedIndex >= 0 ? allLevels[lastServedIndex + 1] : null;
|
|
99
|
+
const shaftTopY = nextLevel
|
|
100
|
+
? (baseYByLevelId.get(nextLevel.id) ?? cumulativeY)
|
|
101
|
+
: lastServedLevel
|
|
102
|
+
? cumulativeY
|
|
103
|
+
: elevator.cabHeight + 0.3;
|
|
104
|
+
return {
|
|
105
|
+
entries,
|
|
106
|
+
defaultEntry,
|
|
107
|
+
shaftBaseY,
|
|
108
|
+
shaftTopY,
|
|
109
|
+
totalHeight: Math.max(shaftTopY - shaftBaseY, elevator.cabHeight + 0.3),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AnyNode, StairNode, StairSegmentNode } from '../../schema';
|
|
2
|
+
/**
|
|
3
|
+
* Stair footprint geometry shared by the slab-opening sync and the
|
|
4
|
+
* alignment-anchor adapters. A stair has no single box footprint: straight
|
|
5
|
+
* stairs are a cumulative chain of `stair-segment` children, curved / spiral
|
|
6
|
+
* stairs are an annular sector stored entirely on the parent. Both reduce to
|
|
7
|
+
* an XZ bounding box here so callers that only need "where does the stair sit
|
|
8
|
+
* in plan" (alignment guides) don't have to re-walk the geometry.
|
|
9
|
+
*
|
|
10
|
+
* All math is in the building-local XZ frame, matching `node.position`.
|
|
11
|
+
*/
|
|
12
|
+
export type StairFootprintAABB = {
|
|
13
|
+
minX: number;
|
|
14
|
+
minZ: number;
|
|
15
|
+
maxX: number;
|
|
16
|
+
maxZ: number;
|
|
17
|
+
};
|
|
18
|
+
type SegmentTransform = {
|
|
19
|
+
position: [number, number, number];
|
|
20
|
+
rotation: number;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* XZ rotation in the stair geometry convention (equivalent to rotating by
|
|
24
|
+
* `-angle` in standard math): positive `angle` turns local +Z toward +X. Every
|
|
25
|
+
* stair helper — slab openings, floor-plan emitter, this footprint — shares it,
|
|
26
|
+
* so anchors line up with the rendered stair.
|
|
27
|
+
*/
|
|
28
|
+
export declare function rotateXZ(x: number, z: number, angle: number): [number, number];
|
|
29
|
+
/**
|
|
30
|
+
* Cumulative per-segment transforms for a straight (segment-chained) stair.
|
|
31
|
+
* Each flight attaches to the previous segment's end; `attachmentSide` rotates
|
|
32
|
+
* the chain ±90° (left / right) or continues straight (front). Positions are in
|
|
33
|
+
* the stair's local frame (before the stair's own `position` / `rotation`).
|
|
34
|
+
*/
|
|
35
|
+
export declare function computeSegmentTransforms(segments: StairSegmentNode[]): SegmentTransform[];
|
|
36
|
+
/**
|
|
37
|
+
* XZ bounding box of a stair's plan footprint, or null when it can't be
|
|
38
|
+
* determined (a straight stair whose segment children aren't in `nodes`).
|
|
39
|
+
* Straight stairs need the children to walk the flight chain; curved / spiral
|
|
40
|
+
* stairs are derived from the parent alone, so `nodes` is optional for them.
|
|
41
|
+
*/
|
|
42
|
+
export declare function stairFootprintAABB(stair: StairNode, nodes?: Readonly<Record<string, AnyNode>>): StairFootprintAABB | null;
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=stair-footprint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stair-footprint.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-footprint.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAa,SAAS,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAEnF;;;;;;;;;GASG;AAEH,MAAM,MAAM,kBAAkB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAE3F,KAAK,gBAAgB,GAAG;IACtB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAI9E;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,EAAE,CAkDzF;AA0GD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,SAAS,EAChB,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACxC,kBAAkB,GAAG,IAAI,CAK3B"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XZ rotation in the stair geometry convention (equivalent to rotating by
|
|
3
|
+
* `-angle` in standard math): positive `angle` turns local +Z toward +X. Every
|
|
4
|
+
* stair helper — slab openings, floor-plan emitter, this footprint — shares it,
|
|
5
|
+
* so anchors line up with the rendered stair.
|
|
6
|
+
*/
|
|
7
|
+
export function rotateXZ(x, z, angle) {
|
|
8
|
+
const cos = Math.cos(angle);
|
|
9
|
+
const sin = Math.sin(angle);
|
|
10
|
+
return [x * cos + z * sin, -x * sin + z * cos];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Cumulative per-segment transforms for a straight (segment-chained) stair.
|
|
14
|
+
* Each flight attaches to the previous segment's end; `attachmentSide` rotates
|
|
15
|
+
* the chain ±90° (left / right) or continues straight (front). Positions are in
|
|
16
|
+
* the stair's local frame (before the stair's own `position` / `rotation`).
|
|
17
|
+
*/
|
|
18
|
+
export function computeSegmentTransforms(segments) {
|
|
19
|
+
const transforms = [];
|
|
20
|
+
let currentX = 0;
|
|
21
|
+
let currentY = 0;
|
|
22
|
+
let currentZ = 0;
|
|
23
|
+
let currentRot = 0;
|
|
24
|
+
for (let index = 0; index < segments.length; index++) {
|
|
25
|
+
const segment = segments[index];
|
|
26
|
+
if (!segment)
|
|
27
|
+
continue;
|
|
28
|
+
if (index === 0) {
|
|
29
|
+
transforms.push({ position: [currentX, currentY, currentZ], rotation: currentRot });
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const previous = segments[index - 1];
|
|
33
|
+
if (!previous)
|
|
34
|
+
continue;
|
|
35
|
+
let attachX = 0;
|
|
36
|
+
let attachZ = 0;
|
|
37
|
+
let rotationDelta = 0;
|
|
38
|
+
switch (segment.attachmentSide) {
|
|
39
|
+
case 'front':
|
|
40
|
+
attachX = 0;
|
|
41
|
+
attachZ = previous.length;
|
|
42
|
+
break;
|
|
43
|
+
case 'left':
|
|
44
|
+
attachX = previous.width / 2;
|
|
45
|
+
attachZ = previous.length / 2;
|
|
46
|
+
rotationDelta = Math.PI / 2;
|
|
47
|
+
break;
|
|
48
|
+
case 'right':
|
|
49
|
+
attachX = -previous.width / 2;
|
|
50
|
+
attachZ = previous.length / 2;
|
|
51
|
+
rotationDelta = -Math.PI / 2;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
const [deltaX, deltaZ] = rotateXZ(attachX, attachZ, currentRot);
|
|
55
|
+
currentX += deltaX;
|
|
56
|
+
currentY += previous.height;
|
|
57
|
+
currentZ += deltaZ;
|
|
58
|
+
currentRot += rotationDelta;
|
|
59
|
+
transforms.push({ position: [currentX, currentY, currentZ], rotation: currentRot });
|
|
60
|
+
}
|
|
61
|
+
return transforms;
|
|
62
|
+
}
|
|
63
|
+
function emptyBox() {
|
|
64
|
+
return {
|
|
65
|
+
minX: Number.POSITIVE_INFINITY,
|
|
66
|
+
minZ: Number.POSITIVE_INFINITY,
|
|
67
|
+
maxX: Number.NEGATIVE_INFINITY,
|
|
68
|
+
maxZ: Number.NEGATIVE_INFINITY,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Grow `box` to include the world-plan point produced by rotating the
|
|
72
|
+
* stair-local point by the stair's rotation and offsetting by its position. */
|
|
73
|
+
function extendByLocal(box, stair, localX, localZ) {
|
|
74
|
+
const [wx, wz] = rotateXZ(localX, localZ, stair.rotation ?? 0);
|
|
75
|
+
const x = stair.position[0] + wx;
|
|
76
|
+
const z = stair.position[2] + wz;
|
|
77
|
+
if (x < box.minX)
|
|
78
|
+
box.minX = x;
|
|
79
|
+
if (x > box.maxX)
|
|
80
|
+
box.maxX = x;
|
|
81
|
+
if (z < box.minZ)
|
|
82
|
+
box.minZ = z;
|
|
83
|
+
if (z > box.maxZ)
|
|
84
|
+
box.maxZ = z;
|
|
85
|
+
}
|
|
86
|
+
function finiteBox(box) {
|
|
87
|
+
return Number.isFinite(box.minX) && Number.isFinite(box.minZ) ? box : null;
|
|
88
|
+
}
|
|
89
|
+
/** Bounding box of a straight stair's segment chain, walking the children. */
|
|
90
|
+
function straightStairAABB(stair, nodes) {
|
|
91
|
+
const segments = (stair.children ?? [])
|
|
92
|
+
.map((childId) => nodes[childId])
|
|
93
|
+
.filter((segment) => segment?.type === 'stair-segment' && segment.visible !== false);
|
|
94
|
+
if (segments.length === 0)
|
|
95
|
+
return null;
|
|
96
|
+
const transforms = computeSegmentTransforms(segments);
|
|
97
|
+
const box = emptyBox();
|
|
98
|
+
segments.forEach((segment, index) => {
|
|
99
|
+
const transform = transforms[index];
|
|
100
|
+
if (!transform)
|
|
101
|
+
return;
|
|
102
|
+
const halfWidth = segment.width / 2;
|
|
103
|
+
// Segment-local footprint: X across the flight, Z along the run from the
|
|
104
|
+
// attachment edge (0) to the far edge (length).
|
|
105
|
+
for (const [cornerX, cornerZ] of [
|
|
106
|
+
[-halfWidth, 0],
|
|
107
|
+
[halfWidth, 0],
|
|
108
|
+
[halfWidth, segment.length],
|
|
109
|
+
[-halfWidth, segment.length],
|
|
110
|
+
]) {
|
|
111
|
+
const [offsetX, offsetZ] = rotateXZ(cornerX, cornerZ, transform.rotation);
|
|
112
|
+
extendByLocal(box, stair, transform.position[0] + offsetX, transform.position[2] + offsetZ);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return finiteBox(box);
|
|
116
|
+
}
|
|
117
|
+
const ARC_SAMPLES = 48;
|
|
118
|
+
/** Bounding box of a curved / spiral stair's annular sector (plus the
|
|
119
|
+
* integrated spiral top landing when present). */
|
|
120
|
+
function arcStairAABB(stair) {
|
|
121
|
+
const isSpiral = stair.stairType === 'spiral';
|
|
122
|
+
const minInnerRadius = isSpiral ? 0.05 : 0.2;
|
|
123
|
+
const innerRadius = Math.max(minInnerRadius, stair.innerRadius ?? (isSpiral ? 0.2 : 0.9));
|
|
124
|
+
const width = Math.max(stair.width ?? 1, 0.4);
|
|
125
|
+
const outerRadius = innerRadius + width;
|
|
126
|
+
let sweep = stair.sweepAngle ?? (isSpiral ? Math.PI * 2 : Math.PI / 2);
|
|
127
|
+
// A full revolution would make the arc degenerate; clamp just under 2π the
|
|
128
|
+
// same way the floor-plan emitter does so the sampled box stays correct.
|
|
129
|
+
if (Math.abs(sweep) >= Math.PI * 2)
|
|
130
|
+
sweep = Math.sign(sweep || 1) * (Math.PI * 2 - 0.001);
|
|
131
|
+
const half = sweep / 2;
|
|
132
|
+
const box = emptyBox();
|
|
133
|
+
// Sample both rims across the sweep — the extremes can fall on either the
|
|
134
|
+
// arc ends or an axis crossing in between, so we need the full sweep.
|
|
135
|
+
for (let step = 0; step <= ARC_SAMPLES; step += 1) {
|
|
136
|
+
const angle = -half + (sweep * step) / ARC_SAMPLES;
|
|
137
|
+
const cos = Math.cos(angle);
|
|
138
|
+
const sin = Math.sin(angle);
|
|
139
|
+
extendByLocal(box, stair, cos * innerRadius, sin * innerRadius);
|
|
140
|
+
extendByLocal(box, stair, cos * outerRadius, sin * outerRadius);
|
|
141
|
+
}
|
|
142
|
+
// Integrated spiral top landing — a rectangle hung off the outer rim.
|
|
143
|
+
if (isSpiral && stair.topLandingMode === 'integrated') {
|
|
144
|
+
const depth = Math.max(stair.topLandingDepth ?? 0.9, 0.1);
|
|
145
|
+
const halfWidth = width / 2;
|
|
146
|
+
for (const [cornerX, cornerZ] of [
|
|
147
|
+
[outerRadius, -halfWidth],
|
|
148
|
+
[outerRadius + depth, -halfWidth],
|
|
149
|
+
[outerRadius + depth, halfWidth],
|
|
150
|
+
[outerRadius, halfWidth],
|
|
151
|
+
]) {
|
|
152
|
+
extendByLocal(box, stair, cornerX, cornerZ);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return finiteBox(box);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* XZ bounding box of a stair's plan footprint, or null when it can't be
|
|
159
|
+
* determined (a straight stair whose segment children aren't in `nodes`).
|
|
160
|
+
* Straight stairs need the children to walk the flight chain; curved / spiral
|
|
161
|
+
* stairs are derived from the parent alone, so `nodes` is optional for them.
|
|
162
|
+
*/
|
|
163
|
+
export function stairFootprintAABB(stair, nodes) {
|
|
164
|
+
if ((stair.stairType ?? 'straight') === 'straight') {
|
|
165
|
+
return nodes ? straightStairAABB(stair, nodes) : null;
|
|
166
|
+
}
|
|
167
|
+
return arcStairAABB(stair);
|
|
168
|
+
}
|