@pascal-app/core 0.9.2 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -10
- package/dist/events/bus.d.ts +21 -2
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +17 -1
- package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/floor-placed-elevation.js +61 -8
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +116 -66
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +319 -441
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +46 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +204 -12
- package/dist/hooks/spatial-grid/support-host-id.d.ts +3 -0
- package/dist/hooks/spatial-grid/support-host-id.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/support-host-id.js +2 -0
- package/dist/hooks/spatial-grid/support-host-patch.d.ts +51 -0
- package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/support-host-patch.js +164 -0
- package/dist/index.d.ts +23 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -10
- package/dist/lib/measurement-geometry.d.ts +4 -0
- package/dist/lib/measurement-geometry.d.ts.map +1 -1
- package/dist/lib/measurement-geometry.js +27 -10
- package/dist/lib/slab-polygon.d.ts.map +1 -1
- package/dist/lib/slab-polygon.js +33 -1
- package/dist/lib/space-detection.d.ts +15 -4
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +232 -91
- package/dist/lib/terrain-brush.d.ts +163 -0
- package/dist/lib/terrain-brush.d.ts.map +1 -0
- package/dist/lib/terrain-brush.js +359 -0
- package/dist/lib/terrain-codec.d.ts +51 -0
- package/dist/lib/terrain-codec.d.ts.map +1 -0
- package/dist/lib/terrain-codec.js +167 -0
- package/dist/lib/terrain-field.d.ts +150 -0
- package/dist/lib/terrain-field.d.ts.map +1 -0
- package/dist/lib/terrain-field.js +248 -0
- package/dist/lib/terrain-raycast.d.ts +44 -0
- package/dist/lib/terrain-raycast.d.ts.map +1 -0
- package/dist/lib/terrain-raycast.js +188 -0
- package/dist/lib/terrain-source.d.ts +60 -0
- package/dist/lib/terrain-source.d.ts.map +1 -0
- package/dist/lib/terrain-source.js +90 -0
- package/dist/lib/terrain-support.d.ts +85 -0
- package/dist/lib/terrain-support.d.ts.map +1 -0
- package/dist/lib/terrain-support.js +218 -0
- package/dist/lib/zone-quantities.d.ts +1 -1
- package/dist/lib/zone-quantities.d.ts.map +1 -1
- package/dist/lib/zone-quantities.js +311 -66
- package/dist/material-library.d.ts +9 -1
- package/dist/material-library.d.ts.map +1 -1
- package/dist/material-library.js +56 -4
- package/dist/registry/handles.d.ts +16 -1
- package/dist/registry/handles.d.ts.map +1 -1
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/subtree.d.ts.map +1 -1
- package/dist/registry/subtree.js +5 -2
- package/dist/registry/types.d.ts +83 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/schema/index.d.ts +6 -3
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +6 -3
- package/dist/schema/nodes/cabinet.d.ts +2 -0
- package/dist/schema/nodes/cabinet.d.ts.map +1 -1
- package/dist/schema/nodes/cabinet.js +4 -2
- package/dist/schema/nodes/ceiling.d.ts +1 -1
- package/dist/schema/nodes/ceiling.d.ts.map +1 -1
- package/dist/schema/nodes/ceiling.js +7 -1
- package/dist/schema/nodes/column.d.ts +1 -0
- package/dist/schema/nodes/column.d.ts.map +1 -1
- package/dist/schema/nodes/column.js +2 -0
- package/dist/schema/nodes/construction-dimension.d.ts +195 -0
- package/dist/schema/nodes/construction-dimension.d.ts.map +1 -0
- package/dist/schema/nodes/construction-dimension.js +151 -0
- package/dist/schema/nodes/door.d.ts +29 -0
- package/dist/schema/nodes/door.d.ts.map +1 -1
- package/dist/schema/nodes/door.js +20 -0
- package/dist/schema/nodes/duct-terminal.d.ts +1 -0
- package/dist/schema/nodes/duct-terminal.d.ts.map +1 -1
- package/dist/schema/nodes/duct-terminal.js +2 -0
- package/dist/schema/nodes/fence.d.ts +2 -0
- package/dist/schema/nodes/fence.d.ts.map +1 -1
- package/dist/schema/nodes/fence.js +8 -0
- package/dist/schema/nodes/hvac-equipment.d.ts +1 -0
- package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -1
- package/dist/schema/nodes/hvac-equipment.js +2 -0
- package/dist/schema/nodes/item.d.ts +1 -0
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +13 -0
- package/dist/schema/nodes/level.d.ts +2 -1
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +12 -33
- package/dist/schema/nodes/shelf.d.ts +1 -0
- package/dist/schema/nodes/shelf.d.ts.map +1 -1
- package/dist/schema/nodes/shelf.js +2 -0
- package/dist/schema/nodes/site.d.ts +9 -0
- package/dist/schema/nodes/site.d.ts.map +1 -1
- package/dist/schema/nodes/site.js +8 -6
- package/dist/schema/nodes/slab.d.ts +5 -0
- package/dist/schema/nodes/slab.d.ts.map +1 -1
- package/dist/schema/nodes/slab.js +14 -2
- package/dist/schema/nodes/spawn.d.ts +1 -0
- package/dist/schema/nodes/spawn.d.ts.map +1 -1
- package/dist/schema/nodes/spawn.js +2 -0
- package/dist/schema/nodes/stair.d.ts +3 -1
- package/dist/schema/nodes/stair.d.ts.map +1 -1
- package/dist/schema/nodes/stair.js +8 -1
- package/dist/schema/nodes/structural-grid.d.ts +27 -0
- package/dist/schema/nodes/structural-grid.d.ts.map +1 -0
- package/dist/schema/nodes/structural-grid.js +17 -0
- package/dist/schema/nodes/wall.d.ts +5 -2
- package/dist/schema/nodes/wall.d.ts.map +1 -1
- package/dist/schema/nodes/wall.js +13 -4
- package/dist/schema/nodes/window.d.ts +29 -0
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/nodes/window.js +18 -0
- package/dist/schema/nodes/zone.d.ts +20 -0
- package/dist/schema/nodes/zone.d.ts.map +1 -1
- package/dist/schema/nodes/zone.js +15 -0
- package/dist/schema/terrain.d.ts +28 -0
- package/dist/schema/terrain.d.ts.map +1 -0
- package/dist/schema/terrain.js +30 -0
- package/dist/schema/types.d.ts +211 -3
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +4 -0
- package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts +80 -0
- package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts.map +1 -0
- package/dist/services/__fixtures__/wall-plane-top-boundary-repro.js +89 -0
- package/dist/services/index.d.ts +2 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -1
- package/dist/services/level-height.d.ts +8 -9
- package/dist/services/level-height.d.ts.map +1 -1
- package/dist/services/level-height.js +48 -16
- package/dist/services/storey.d.ts +103 -0
- package/dist/services/storey.d.ts.map +1 -0
- package/dist/services/storey.js +296 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +47 -3
- package/dist/store/history-control.d.ts +20 -0
- package/dist/store/history-control.d.ts.map +1 -1
- package/dist/store/history-control.js +110 -13
- package/dist/store/use-live-terrain.d.ts +65 -0
- package/dist/store/use-live-terrain.d.ts.map +1 -0
- package/dist/store/use-live-terrain.js +31 -0
- package/dist/store/use-live-transforms.d.ts +8 -0
- package/dist/store/use-live-transforms.d.ts.map +1 -1
- package/dist/store/use-scene.d.ts +31 -2
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +638 -8
- package/dist/systems/elevator/elevator-opening-system.d.ts +1 -0
- package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-opening-system.js +27 -21
- package/dist/systems/elevator/elevator-service.d.ts +0 -2
- package/dist/systems/elevator/elevator-service.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-service.js +2 -24
- package/dist/systems/slab/slab-placement.d.ts +11 -0
- package/dist/systems/slab/slab-placement.d.ts.map +1 -0
- package/dist/systems/slab/slab-placement.js +14 -0
- package/dist/systems/slab/slab-support.d.ts +150 -0
- package/dist/systems/slab/slab-support.d.ts.map +1 -0
- package/dist/systems/slab/slab-support.js +575 -0
- package/dist/systems/stair/stair-opening-preview.d.ts +1060 -935
- package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.js +18 -15
- package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-system.js +32 -4
- package/dist/systems/stair/stair-rise.d.ts +22 -0
- package/dist/systems/stair/stair-rise.d.ts.map +1 -0
- package/dist/systems/stair/stair-rise.js +79 -0
- package/dist/systems/wall/wall-curve.d.ts +10 -0
- package/dist/systems/wall/wall-curve.d.ts.map +1 -1
- package/dist/systems/wall/wall-curve.js +1 -1
- package/dist/systems/wall/wall-mitering.js +6 -6
- package/dist/systems/wall/wall-top.d.ts +40 -0
- package/dist/systems/wall/wall-top.d.ts.map +1 -0
- package/dist/systems/wall/wall-top.js +46 -0
- package/dist/utils/clone-scene-graph.d.ts.map +1 -1
- package/dist/utils/clone-scene-graph.js +34 -3
- package/dist/utils/heal-scene-graph.d.ts +1 -1
- package/dist/utils/heal-scene-graph.d.ts.map +1 -1
- package/dist/utils/heal-scene-graph.js +25 -9
- package/dist/validation/validate-build-json.d.ts +2 -0
- package/dist/validation/validate-build-json.d.ts.map +1 -1
- package/dist/validation/validate-build-json.js +59 -3
- package/package.json +2 -2
- package/dist/hooks/scene-registry/scene-registry.test.d.ts +0 -2
- package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +0 -1
- package/dist/hooks/scene-registry/scene-registry.test.js +0 -26
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +0 -2
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +0 -1
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +0 -366
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +0 -2
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +0 -1
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +0 -363
- package/dist/lib/measurement-geometry.test.d.ts +0 -2
- package/dist/lib/measurement-geometry.test.d.ts.map +0 -1
- package/dist/lib/measurement-geometry.test.js +0 -102
- package/dist/lib/slab-polygon.test.d.ts +0 -2
- package/dist/lib/slab-polygon.test.d.ts.map +0 -1
- package/dist/lib/slab-polygon.test.js +0 -635
- package/dist/lib/space-detection-pause.test.d.ts +0 -2
- package/dist/lib/space-detection-pause.test.d.ts.map +0 -1
- package/dist/lib/space-detection-pause.test.js +0 -42
- package/dist/lib/space-detection.test.d.ts +0 -2
- package/dist/lib/space-detection.test.d.ts.map +0 -1
- package/dist/lib/space-detection.test.js +0 -376
- package/dist/lib/zone-quantities.test.d.ts +0 -2
- package/dist/lib/zone-quantities.test.d.ts.map +0 -1
- package/dist/lib/zone-quantities.test.js +0 -82
- package/dist/registry/registry.test.d.ts +0 -2
- package/dist/registry/registry.test.d.ts.map +0 -1
- package/dist/registry/registry.test.js +0 -196
- package/dist/registry/relations-resolver.test.d.ts +0 -2
- package/dist/registry/relations-resolver.test.d.ts.map +0 -1
- package/dist/registry/relations-resolver.test.js +0 -183
- package/dist/registry/scene-api.test.d.ts +0 -2
- package/dist/registry/scene-api.test.d.ts.map +0 -1
- package/dist/registry/scene-api.test.js +0 -183
- package/dist/registry/subtree.test.d.ts +0 -2
- package/dist/registry/subtree.test.d.ts.map +0 -1
- package/dist/registry/subtree.test.js +0 -163
- package/dist/schema/asset-url.test.d.ts +0 -2
- package/dist/schema/asset-url.test.d.ts.map +0 -1
- package/dist/schema/asset-url.test.js +0 -134
- package/dist/schema/material.test.d.ts +0 -2
- package/dist/schema/material.test.d.ts.map +0 -1
- package/dist/schema/material.test.js +0 -43
- package/dist/schema/nodes/measurement.test.d.ts +0 -2
- package/dist/schema/nodes/measurement.test.d.ts.map +0 -1
- package/dist/schema/nodes/measurement.test.js +0 -139
- package/dist/schema/nodes/ridge-vent-defaults.test.d.ts +0 -2
- package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +0 -1
- package/dist/schema/nodes/ridge-vent-defaults.test.js +0 -205
- package/dist/schema/nodes/roof-segment-shape.test.d.ts +0 -2
- package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +0 -1
- package/dist/schema/nodes/roof-segment-shape.test.js +0 -175
- package/dist/schema/nodes/roof-segment-surface.test.d.ts +0 -2
- package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +0 -1
- package/dist/schema/nodes/roof-segment-surface.test.js +0 -48
- package/dist/schema/nodes/roof-segment-trim.test.d.ts +0 -2
- package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +0 -1
- package/dist/schema/nodes/roof-segment-trim.test.js +0 -107
- package/dist/schema/nodes/roof-segment-walls.test.d.ts +0 -2
- package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +0 -1
- package/dist/schema/nodes/roof-segment-walls.test.js +0 -92
- package/dist/schema/nodes/wall.test.d.ts +0 -2
- package/dist/schema/nodes/wall.test.d.ts.map +0 -1
- package/dist/schema/nodes/wall.test.js +0 -208
- package/dist/services/alignment-anchors.test.d.ts +0 -2
- package/dist/services/alignment-anchors.test.d.ts.map +0 -1
- package/dist/services/alignment-anchors.test.js +0 -383
- package/dist/services/alignment.test.d.ts +0 -2
- package/dist/services/alignment.test.d.ts.map +0 -1
- package/dist/services/alignment.test.js +0 -92
- package/dist/services/drag-session.test.d.ts +0 -2
- package/dist/services/drag-session.test.d.ts.map +0 -1
- package/dist/services/drag-session.test.js +0 -219
- package/dist/services/hosting.test.d.ts +0 -2
- package/dist/services/hosting.test.d.ts.map +0 -1
- package/dist/services/hosting.test.js +0 -230
- package/dist/services/movement.test.d.ts +0 -2
- package/dist/services/movement.test.d.ts.map +0 -1
- package/dist/services/movement.test.js +0 -133
- package/dist/services/opening-guides.test.d.ts +0 -2
- package/dist/services/opening-guides.test.d.ts.map +0 -1
- package/dist/services/opening-guides.test.js +0 -208
- package/dist/services/port-connectivity.test.d.ts +0 -2
- package/dist/services/port-connectivity.test.d.ts.map +0 -1
- package/dist/services/port-connectivity.test.js +0 -297
- package/dist/services/riser-diagram.test.d.ts +0 -2
- package/dist/services/riser-diagram.test.d.ts.map +0 -1
- package/dist/services/riser-diagram.test.js +0 -66
- package/dist/services/single-undo-dance.test.d.ts +0 -2
- package/dist/services/single-undo-dance.test.d.ts.map +0 -1
- package/dist/services/single-undo-dance.test.js +0 -210
- package/dist/services/snap.test.d.ts +0 -2
- package/dist/services/snap.test.d.ts.map +0 -1
- package/dist/services/snap.test.js +0 -137
- package/dist/services/system-graph.test.d.ts +0 -2
- package/dist/services/system-graph.test.d.ts.map +0 -1
- package/dist/services/system-graph.test.js +0 -138
- package/dist/services/validate-dwv.test.d.ts +0 -2
- package/dist/services/validate-dwv.test.d.ts.map +0 -1
- package/dist/services/validate-dwv.test.js +0 -109
- package/dist/store/actions/node-mutation-sanitize.test.d.ts +0 -2
- package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +0 -1
- package/dist/store/actions/node-mutation-sanitize.test.js +0 -147
- package/dist/store/actions/reparent.test.d.ts +0 -2
- package/dist/store/actions/reparent.test.d.ts.map +0 -1
- package/dist/store/actions/reparent.test.js +0 -212
- package/dist/store/actions/ridge-vent-update.test.d.ts +0 -2
- package/dist/store/actions/ridge-vent-update.test.d.ts.map +0 -1
- package/dist/store/actions/ridge-vent-update.test.js +0 -264
- package/dist/store/use-scene-dirty-tracking.test.d.ts +0 -2
- package/dist/store/use-scene-dirty-tracking.test.d.ts.map +0 -1
- package/dist/store/use-scene-dirty-tracking.test.js +0 -68
- package/dist/store/use-scene-elevator-migration.test.d.ts +0 -2
- package/dist/store/use-scene-elevator-migration.test.d.ts.map +0 -1
- package/dist/store/use-scene-elevator-migration.test.js +0 -109
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts +0 -2
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +0 -1
- package/dist/store/use-scene-measurement-roundtrip.test.js +0 -97
- package/dist/store/use-scene-plugins.test.d.ts +0 -2
- package/dist/store/use-scene-plugins.test.d.ts.map +0 -1
- package/dist/store/use-scene-plugins.test.js +0 -51
- package/dist/store/use-scene-wall-slot-migration.test.d.ts +0 -2
- package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +0 -1
- package/dist/store/use-scene-wall-slot-migration.test.js +0 -210
- package/dist/store/use-scene-window-migration.test.d.ts +0 -2
- package/dist/store/use-scene-window-migration.test.d.ts.map +0 -1
- package/dist/store/use-scene-window-migration.test.js +0 -85
- package/dist/systems/elevator/elevator-opening-sync.test.d.ts +0 -2
- package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +0 -1
- package/dist/systems/elevator/elevator-opening-sync.test.js +0 -146
- package/dist/systems/elevator/elevator-runtime.test.d.ts +0 -2
- package/dist/systems/elevator/elevator-runtime.test.d.ts.map +0 -1
- package/dist/systems/elevator/elevator-runtime.test.js +0 -64
- package/dist/systems/fence/fence-spline.test.d.ts +0 -2
- package/dist/systems/fence/fence-spline.test.d.ts.map +0 -1
- package/dist/systems/fence/fence-spline.test.js +0 -136
- package/dist/systems/stair/stair-opening-preview.test.d.ts +0 -2
- package/dist/systems/stair/stair-opening-preview.test.d.ts.map +0 -1
- package/dist/systems/stair/stair-opening-preview.test.js +0 -80
- package/dist/systems/stair/stair-opening-sync.test.d.ts +0 -2
- package/dist/systems/stair/stair-opening-sync.test.d.ts.map +0 -1
- package/dist/systems/stair/stair-opening-sync.test.js +0 -485
- package/dist/systems/wall/wall-mitering.test.d.ts +0 -2
- package/dist/systems/wall/wall-mitering.test.d.ts.map +0 -1
- package/dist/systems/wall/wall-mitering.test.js +0 -63
- package/dist/utils/clone-scene-graph.test.d.ts +0 -2
- package/dist/utils/clone-scene-graph.test.d.ts.map +0 -1
- package/dist/utils/clone-scene-graph.test.js +0 -62
- package/dist/utils/heal-scene-graph.test.d.ts +0 -2
- package/dist/utils/heal-scene-graph.test.d.ts.map +0 -1
- package/dist/utils/heal-scene-graph.test.js +0 -88
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scene → `TerrainField`, cached.
|
|
3
|
+
*
|
|
4
|
+
* Every consumer that needs the ground — the mesh builder, the raycast, the
|
|
5
|
+
* placement predicate, the collider, the 2D view — starts here. It exists to
|
|
6
|
+
* solve one problem: the scene stores terrain as base64 (`site.terrain`), and
|
|
7
|
+
* decoding that per frame, or per pointer-move, would be absurd.
|
|
8
|
+
*
|
|
9
|
+
* The cache is a `WeakMap` keyed on the **`TerrainData` object identity**, which
|
|
10
|
+
* is a correct version key because the scene store is immutable-by-convention:
|
|
11
|
+
* `updateNode` produces a new node object with a new `terrain` object, so a
|
|
12
|
+
* changed terrain necessarily misses the cache and a stale entry is unreachable.
|
|
13
|
+
* No invalidation call, no version counter, nothing to forget to bump.
|
|
14
|
+
*
|
|
15
|
+
* `commitTerrainField` closes the loop: it encodes a field *and* primes the cache
|
|
16
|
+
* with the field it just encoded. That is what makes a sculpt stroke free — after
|
|
17
|
+
* the commit, the next read is a cache hit, so a stroke never decodes base64 even
|
|
18
|
+
* once, let alone per dab.
|
|
19
|
+
*/
|
|
20
|
+
import type { SiteNode } from '../schema/nodes/site';
|
|
21
|
+
import type { TerrainData } from '../schema/terrain';
|
|
22
|
+
import { createTerrainField, type TerrainField } from './terrain-field';
|
|
23
|
+
/**
|
|
24
|
+
* The terrain field for a site, or `null` when the site has no sculpted terrain.
|
|
25
|
+
*
|
|
26
|
+
* `null` means flat ground at the datum. It is deliberately not "a flat field":
|
|
27
|
+
* callers use `null` to take their existing flat-ground fast path, which keeps
|
|
28
|
+
* every scene that has never touched terrain — the overwhelming majority —
|
|
29
|
+
* running exactly the code it ran before terrain existed. That property is worth
|
|
30
|
+
* more than the uniformity of always having a field.
|
|
31
|
+
*
|
|
32
|
+
* An in-flight sculpt stroke wins over the persisted data. Resolving that *here*
|
|
33
|
+
* rather than per caller is what makes a stroke consistent: the mesh, the pointer
|
|
34
|
+
* raycast, the placement predicate, the collider, and the 2D view all see the
|
|
35
|
+
* same ground mid-drag without any of them knowing a stroke exists. The
|
|
36
|
+
* alternative — each consumer checking the live store — is how a drag ends up
|
|
37
|
+
* showing a hill the raycast cannot hit.
|
|
38
|
+
*/
|
|
39
|
+
export declare function terrainFieldOf(site: (Pick<SiteNode, 'terrain'> & {
|
|
40
|
+
id?: string;
|
|
41
|
+
}) | null | undefined): TerrainField | null;
|
|
42
|
+
/**
|
|
43
|
+
* The field a site *would* sculpt into: its existing terrain, or a fresh flat
|
|
44
|
+
* field sized to `options`.
|
|
45
|
+
*
|
|
46
|
+
* Separate from `terrainFieldOf` because the read path wants `null` for "no
|
|
47
|
+
* terrain" and the write path wants something to write into. Collapsing them
|
|
48
|
+
* would force every reader to allocate a field it does not need.
|
|
49
|
+
*/
|
|
50
|
+
export declare function terrainFieldForEdit(site: Pick<SiteNode, 'terrain'> | null | undefined, options?: Parameters<typeof createTerrainField>[0]): TerrainField;
|
|
51
|
+
/**
|
|
52
|
+
* Encode a field for persistence and prime the read cache with it.
|
|
53
|
+
*
|
|
54
|
+
* Call this instead of `encodeTerrainField` whenever the result is going into the
|
|
55
|
+
* scene. The returned data is what to write to `site.terrain`; the cache priming
|
|
56
|
+
* means the very next `terrainFieldOf` on the updated node returns this exact
|
|
57
|
+
* field object without decoding.
|
|
58
|
+
*/
|
|
59
|
+
export declare function commitTerrainField(field: TerrainField): TerrainData;
|
|
60
|
+
//# sourceMappingURL=terrain-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terrain-source.d.ts","sourceRoot":"","sources":["../../src/lib/terrain-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAGpD,OAAO,EAAE,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAUvE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,GAAG;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACrE,YAAY,GAAG,IAAI,CAkBrB;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,EAClD,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC,CAAC,GACjD,YAAY,CAEd;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,GAAG,WAAW,CAInE"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scene → `TerrainField`, cached.
|
|
3
|
+
*
|
|
4
|
+
* Every consumer that needs the ground — the mesh builder, the raycast, the
|
|
5
|
+
* placement predicate, the collider, the 2D view — starts here. It exists to
|
|
6
|
+
* solve one problem: the scene stores terrain as base64 (`site.terrain`), and
|
|
7
|
+
* decoding that per frame, or per pointer-move, would be absurd.
|
|
8
|
+
*
|
|
9
|
+
* The cache is a `WeakMap` keyed on the **`TerrainData` object identity**, which
|
|
10
|
+
* is a correct version key because the scene store is immutable-by-convention:
|
|
11
|
+
* `updateNode` produces a new node object with a new `terrain` object, so a
|
|
12
|
+
* changed terrain necessarily misses the cache and a stale entry is unreachable.
|
|
13
|
+
* No invalidation call, no version counter, nothing to forget to bump.
|
|
14
|
+
*
|
|
15
|
+
* `commitTerrainField` closes the loop: it encodes a field *and* primes the cache
|
|
16
|
+
* with the field it just encoded. That is what makes a sculpt stroke free — after
|
|
17
|
+
* the commit, the next read is a cache hit, so a stroke never decodes base64 even
|
|
18
|
+
* once, let alone per dab.
|
|
19
|
+
*/
|
|
20
|
+
import useLiveTerrain from '../store/use-live-terrain';
|
|
21
|
+
import { decodeTerrainField, encodeTerrainField } from './terrain-codec';
|
|
22
|
+
import { createTerrainField } from './terrain-field';
|
|
23
|
+
const fieldCache = new WeakMap();
|
|
24
|
+
/**
|
|
25
|
+
* Sites whose `terrain` failed to decode. Prevents re-attempting a doomed decode
|
|
26
|
+
* on every frame — the corrupt-scene path must be cheap, not just survivable.
|
|
27
|
+
*/
|
|
28
|
+
const failedDecodes = new WeakSet();
|
|
29
|
+
/**
|
|
30
|
+
* The terrain field for a site, or `null` when the site has no sculpted terrain.
|
|
31
|
+
*
|
|
32
|
+
* `null` means flat ground at the datum. It is deliberately not "a flat field":
|
|
33
|
+
* callers use `null` to take their existing flat-ground fast path, which keeps
|
|
34
|
+
* every scene that has never touched terrain — the overwhelming majority —
|
|
35
|
+
* running exactly the code it ran before terrain existed. That property is worth
|
|
36
|
+
* more than the uniformity of always having a field.
|
|
37
|
+
*
|
|
38
|
+
* An in-flight sculpt stroke wins over the persisted data. Resolving that *here*
|
|
39
|
+
* rather than per caller is what makes a stroke consistent: the mesh, the pointer
|
|
40
|
+
* raycast, the placement predicate, the collider, and the 2D view all see the
|
|
41
|
+
* same ground mid-drag without any of them knowing a stroke exists. The
|
|
42
|
+
* alternative — each consumer checking the live store — is how a drag ends up
|
|
43
|
+
* showing a hill the raycast cannot hit.
|
|
44
|
+
*/
|
|
45
|
+
export function terrainFieldOf(site) {
|
|
46
|
+
if (site?.id) {
|
|
47
|
+
const live = useLiveTerrain.getState().fieldOf(site.id);
|
|
48
|
+
if (live)
|
|
49
|
+
return live;
|
|
50
|
+
}
|
|
51
|
+
const data = site?.terrain;
|
|
52
|
+
if (!data)
|
|
53
|
+
return null;
|
|
54
|
+
const cached = fieldCache.get(data);
|
|
55
|
+
if (cached)
|
|
56
|
+
return cached;
|
|
57
|
+
if (failedDecodes.has(data))
|
|
58
|
+
return null;
|
|
59
|
+
const field = decodeTerrainField(data);
|
|
60
|
+
if (!field) {
|
|
61
|
+
failedDecodes.add(data);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
fieldCache.set(data, field);
|
|
65
|
+
return field;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The field a site *would* sculpt into: its existing terrain, or a fresh flat
|
|
69
|
+
* field sized to `options`.
|
|
70
|
+
*
|
|
71
|
+
* Separate from `terrainFieldOf` because the read path wants `null` for "no
|
|
72
|
+
* terrain" and the write path wants something to write into. Collapsing them
|
|
73
|
+
* would force every reader to allocate a field it does not need.
|
|
74
|
+
*/
|
|
75
|
+
export function terrainFieldForEdit(site, options) {
|
|
76
|
+
return terrainFieldOf(site) ?? createTerrainField(options);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Encode a field for persistence and prime the read cache with it.
|
|
80
|
+
*
|
|
81
|
+
* Call this instead of `encodeTerrainField` whenever the result is going into the
|
|
82
|
+
* scene. The returned data is what to write to `site.terrain`; the cache priming
|
|
83
|
+
* means the very next `terrainFieldOf` on the updated node returns this exact
|
|
84
|
+
* field object without decoding.
|
|
85
|
+
*/
|
|
86
|
+
export function commitTerrainField(field) {
|
|
87
|
+
const data = encodeTerrainField(field);
|
|
88
|
+
fieldCache.set(data, field);
|
|
89
|
+
return data;
|
|
90
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terrain as the level base — the third and last place flat ground was assumed.
|
|
3
|
+
*
|
|
4
|
+
* `getFloorPlacedElevation` returns a *lift* added to a node's level-local Y, and
|
|
5
|
+
* two of its `0`-returns mean "supported by the level base". This module is what
|
|
6
|
+
* those two branches call so that "the level base" can mean the sculpted ground
|
|
7
|
+
* instead of the plane `y = 0`. The other `0`-returns are `+0` no-ops for
|
|
8
|
+
* wall/ceiling-attached items, non-`level` parents, and broken graphs; substituting
|
|
9
|
+
* terrain there would lift sconces off walls, so they stay `0` forever.
|
|
10
|
+
*
|
|
11
|
+
* The whole file is pure — nodes in, metres out — which is what lets the stacking
|
|
12
|
+
* matrix be tested per kind without a canvas.
|
|
13
|
+
*/
|
|
14
|
+
import type { AnyNode } from '../schema';
|
|
15
|
+
/**
|
|
16
|
+
* World Y of the site datum — the plane terrain heights are measured from.
|
|
17
|
+
*
|
|
18
|
+
* Terrain vertices are absolute world coordinates (the site group renders at the
|
|
19
|
+
* origin and `terrain-geometry` writes `origin + col * spacing` straight into the
|
|
20
|
+
* buffer), so the datum is a constant rather than a site property. It is named
|
|
21
|
+
* because the *comparison* against it is load-bearing, not the value: it is what
|
|
22
|
+
* separates "the ground" from every other horizontal surface in the scene.
|
|
23
|
+
*/
|
|
24
|
+
export declare const SITE_DATUM_Y = 0;
|
|
25
|
+
/**
|
|
26
|
+
* How close a surface must sit to the datum to count as the ground.
|
|
27
|
+
*
|
|
28
|
+
* Loose enough to absorb float drift through a level's world matrix, tight enough
|
|
29
|
+
* that a ground slab (5 cm) still reads as a built surface rather than as terrain.
|
|
30
|
+
*/
|
|
31
|
+
export declare const SITE_DATUM_EPSILON = 0.0001;
|
|
32
|
+
/** True when a world-space height is the site datum — i.e. the terrain's plane. */
|
|
33
|
+
export declare function isSiteDatum(worldY: number): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Whether the sculpted ground is what a node on `levelId` stands on — i.e.
|
|
36
|
+
* whether moving the terrain moves that node.
|
|
37
|
+
*
|
|
38
|
+
* Split out of `terrainSupportLift` so the invalidation rule that re-elevates
|
|
39
|
+
* nodes after a sculpt (`markTerrainSupportDependents`) tests grade-ness with
|
|
40
|
+
* the *same* predicate the resolver does. Two copies of this test would drift,
|
|
41
|
+
* and the failure is silent: a level the resolver drapes but the dirty rule
|
|
42
|
+
* skips leaves its contents frozen at the height the ground used to be.
|
|
43
|
+
*
|
|
44
|
+
* Deliberately independent of whether a field exists. A stroke that clears the
|
|
45
|
+
* terrain has to re-elevate exactly the nodes a stroke that creates it does, and
|
|
46
|
+
* asking about the field here would mark none of them on the way back down.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isLevelAtSiteDatum(nodes: Record<string, AnyNode>, levelId: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* The lift, in level-local metres, that the sculpted ground provides to a node on
|
|
51
|
+
* `levelId` at level-local `x`/`z`. Null when terrain does not support this node —
|
|
52
|
+
* no sculpted ground, an unresolvable level, or a storey whose floor is not at
|
|
53
|
+
* grade.
|
|
54
|
+
*
|
|
55
|
+
* Null rather than 0 so callers keep their existing flat-ground path verbatim.
|
|
56
|
+
* That is what makes this change invisible to every scene that never touched
|
|
57
|
+
* terrain, which is nearly all of them.
|
|
58
|
+
*/
|
|
59
|
+
export declare function terrainSupportLift(nodes: Record<string, AnyNode>, levelId: string, x: number, z: number): number | null;
|
|
60
|
+
/**
|
|
61
|
+
* **The level base, in level-local metres, at level-local `x`/`z`** — the surface
|
|
62
|
+
* a node rests on when nothing built is under it. Sculpted ground where terrain
|
|
63
|
+
* supports this storey, `0` everywhere else.
|
|
64
|
+
*
|
|
65
|
+
* This is the one function every "nothing is under me, so I'm at zero" site in
|
|
66
|
+
* the codebase should call, and the reason it exists separately from
|
|
67
|
+
* {@link terrainSupportLift}: that function answers *"is there terrain here"*
|
|
68
|
+
* (nullable, so a caller can branch), while this one answers *"how high is the
|
|
69
|
+
* floor of the world here"* (total, so a caller does not have to know terrain
|
|
70
|
+
* exists). Spelling the second question `terrainSupportLift(…) ?? 0` at each
|
|
71
|
+
* site is what made terrain opt-in per kind — every new consumer had to remember
|
|
72
|
+
* to ask, and the ones that forgot silently assumed the plane `y = 0`. Kinds
|
|
73
|
+
* inherit terrain by resolving their base through here instead of hardcoding a
|
|
74
|
+
* zero; nothing has to be registered for that to work.
|
|
75
|
+
*
|
|
76
|
+
* Callers that must distinguish "flat ground" from "a built surface flush with
|
|
77
|
+
* the storey base" still need {@link terrainSupportLift}'s null — both read `0`
|
|
78
|
+
* here and only the first follows a hillside.
|
|
79
|
+
*/
|
|
80
|
+
export declare function levelBaseElevationAt(nodes: Record<string, AnyNode>, levelId: string, x: number, z: number): number;
|
|
81
|
+
/** Record that `type`'s geometry builder resolved its origin from the ground. */
|
|
82
|
+
export declare function noteLevelBaseConsumer(type: string): void;
|
|
83
|
+
/** Whether `type`'s geometry has to be rebuilt when the sculpted ground moves. */
|
|
84
|
+
export declare function isLevelBaseConsumer(type: string): boolean;
|
|
85
|
+
//# sourceMappingURL=terrain-support.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terrain-support.d.ts","sourceRoot":"","sources":["../../src/lib/terrain-support.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAa,MAAM,WAAW,CAAA;AAOnD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,IAAI,CAAA;AAE7B;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,SAAO,CAAA;AAEtC,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;AAoGD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAE3F;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,EACf,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,GACR,MAAM,GAAG,IAAI,CAYf;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,EACf,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,GACR,MAAM,CAER;AA0BD,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAExD;AAED,kFAAkF;AAClF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzD"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terrain as the level base — the third and last place flat ground was assumed.
|
|
3
|
+
*
|
|
4
|
+
* `getFloorPlacedElevation` returns a *lift* added to a node's level-local Y, and
|
|
5
|
+
* two of its `0`-returns mean "supported by the level base". This module is what
|
|
6
|
+
* those two branches call so that "the level base" can mean the sculpted ground
|
|
7
|
+
* instead of the plane `y = 0`. The other `0`-returns are `+0` no-ops for
|
|
8
|
+
* wall/ceiling-attached items, non-`level` parents, and broken graphs; substituting
|
|
9
|
+
* terrain there would lift sconces off walls, so they stay `0` forever.
|
|
10
|
+
*
|
|
11
|
+
* The whole file is pure — nodes in, metres out — which is what lets the stacking
|
|
12
|
+
* matrix be tested per kind without a canvas.
|
|
13
|
+
*/
|
|
14
|
+
import { getLevelElevations } from '../services/storey';
|
|
15
|
+
import { heightAt } from './terrain-field';
|
|
16
|
+
import { terrainFieldOf } from './terrain-source';
|
|
17
|
+
/**
|
|
18
|
+
* World Y of the site datum — the plane terrain heights are measured from.
|
|
19
|
+
*
|
|
20
|
+
* Terrain vertices are absolute world coordinates (the site group renders at the
|
|
21
|
+
* origin and `terrain-geometry` writes `origin + col * spacing` straight into the
|
|
22
|
+
* buffer), so the datum is a constant rather than a site property. It is named
|
|
23
|
+
* because the *comparison* against it is load-bearing, not the value: it is what
|
|
24
|
+
* separates "the ground" from every other horizontal surface in the scene.
|
|
25
|
+
*/
|
|
26
|
+
export const SITE_DATUM_Y = 0;
|
|
27
|
+
/**
|
|
28
|
+
* How close a surface must sit to the datum to count as the ground.
|
|
29
|
+
*
|
|
30
|
+
* Loose enough to absorb float drift through a level's world matrix, tight enough
|
|
31
|
+
* that a ground slab (5 cm) still reads as a built surface rather than as terrain.
|
|
32
|
+
*/
|
|
33
|
+
export const SITE_DATUM_EPSILON = 1e-4;
|
|
34
|
+
/** True when a world-space height is the site datum — i.e. the terrain's plane. */
|
|
35
|
+
export function isSiteDatum(worldY) {
|
|
36
|
+
return Math.abs(worldY - SITE_DATUM_Y) < SITE_DATUM_EPSILON;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `getLevelElevations` memoized on the `nodes` record identity.
|
|
40
|
+
*
|
|
41
|
+
* Without this the stacking resolver is O(N) per node and therefore O(N²) per
|
|
42
|
+
* frame, since `FloorElevationSystem` calls it for every dirty floor-placed node.
|
|
43
|
+
* The scene store is immutable-by-convention, so the record identity is a correct
|
|
44
|
+
* version key — the same reasoning (and the same `WeakMap` shape) as
|
|
45
|
+
* `terrain-source`'s field cache and the spatial grid's `levelWallsCache`.
|
|
46
|
+
*/
|
|
47
|
+
const levelElevationCache = new WeakMap();
|
|
48
|
+
function cachedLevelElevations(nodes) {
|
|
49
|
+
const cached = levelElevationCache.get(nodes);
|
|
50
|
+
if (cached)
|
|
51
|
+
return cached;
|
|
52
|
+
const elevations = getLevelElevations(nodes);
|
|
53
|
+
levelElevationCache.set(nodes, elevations);
|
|
54
|
+
return elevations;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `siteOf` memoized on the `nodes` record identity, for the same reason
|
|
58
|
+
* `levelElevationCache` exists: the resolver is called per wall per frame (the
|
|
59
|
+
* spatial grid) and per wall per store update (the space-detection trigger), so
|
|
60
|
+
* an O(N) scan inside it makes those callers O(N²).
|
|
61
|
+
*/
|
|
62
|
+
const siteCache = new WeakMap();
|
|
63
|
+
function siteOf(nodes) {
|
|
64
|
+
const cached = siteCache.get(nodes);
|
|
65
|
+
if (cached !== undefined)
|
|
66
|
+
return cached;
|
|
67
|
+
let site = null;
|
|
68
|
+
for (const node of Object.values(nodes)) {
|
|
69
|
+
if (node?.type === 'site') {
|
|
70
|
+
site = node;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
siteCache.set(nodes, site);
|
|
75
|
+
return site;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Level-local XZ → world XZ, through the building's transform.
|
|
79
|
+
*
|
|
80
|
+
* A level group carries only a Y offset (`LevelSystem` writes `position.y` and
|
|
81
|
+
* nothing else), so level-local XZ is building-local XZ; the building carries the
|
|
82
|
+
* site-space placement, and the site renders at the origin. Only the Y rotation
|
|
83
|
+
* matters — a building tipped in X or Z is not a thing the editor can produce.
|
|
84
|
+
*/
|
|
85
|
+
function levelLocalToWorldXZ(building, x, z) {
|
|
86
|
+
if (!building)
|
|
87
|
+
return [x, z];
|
|
88
|
+
const angle = building.rotation?.[1] ?? 0;
|
|
89
|
+
const [px, , pz] = building.position ?? [0, 0, 0];
|
|
90
|
+
if (angle === 0)
|
|
91
|
+
return [x + px, z + pz];
|
|
92
|
+
const cos = Math.cos(angle);
|
|
93
|
+
const sin = Math.sin(angle);
|
|
94
|
+
return [cos * x + sin * z + px, -sin * x + cos * z + pz];
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Where a level sits relative to the datum, for the levels terrain can support.
|
|
98
|
+
*
|
|
99
|
+
* **Only the storey the level stack puts at the datum follows terrain.** An upper
|
|
100
|
+
* storey has a real floor, and draping it would sink its contents into the
|
|
101
|
+
* hillside under the building. The test is that storey's own base height against
|
|
102
|
+
* the datum — deliberately asked of `getLevelElevations`, the same function the
|
|
103
|
+
* renderer positions level groups with, so placement can never hold a second
|
|
104
|
+
* opinion about which storey is on the ground.
|
|
105
|
+
*
|
|
106
|
+
* Which storey that is follows from the stack, not from the ordinal: today the
|
|
107
|
+
* lowest ordinal anchors at 0, so a building with an ordinal `-1` level has *that*
|
|
108
|
+
* level at grade and its ground floor a storey up. When genuine below-grade
|
|
109
|
+
* stacking lands, this rule tracks it with no change here — an excavated basement
|
|
110
|
+
* will stop being at the datum, and stop being draped, on its own.
|
|
111
|
+
*/
|
|
112
|
+
function levelDatum(nodes, levelId) {
|
|
113
|
+
const elevation = cachedLevelElevations(nodes).get(levelId);
|
|
114
|
+
if (!elevation)
|
|
115
|
+
return null;
|
|
116
|
+
const building = elevation.buildingId
|
|
117
|
+
? (nodes[elevation.buildingId] ?? null)
|
|
118
|
+
: null;
|
|
119
|
+
const datum = building?.position?.[1] ?? 0;
|
|
120
|
+
const baseWorldY = datum + elevation.baseY;
|
|
121
|
+
if (!isSiteDatum(baseWorldY))
|
|
122
|
+
return null;
|
|
123
|
+
return { building, baseWorldY };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Whether the sculpted ground is what a node on `levelId` stands on — i.e.
|
|
127
|
+
* whether moving the terrain moves that node.
|
|
128
|
+
*
|
|
129
|
+
* Split out of `terrainSupportLift` so the invalidation rule that re-elevates
|
|
130
|
+
* nodes after a sculpt (`markTerrainSupportDependents`) tests grade-ness with
|
|
131
|
+
* the *same* predicate the resolver does. Two copies of this test would drift,
|
|
132
|
+
* and the failure is silent: a level the resolver drapes but the dirty rule
|
|
133
|
+
* skips leaves its contents frozen at the height the ground used to be.
|
|
134
|
+
*
|
|
135
|
+
* Deliberately independent of whether a field exists. A stroke that clears the
|
|
136
|
+
* terrain has to re-elevate exactly the nodes a stroke that creates it does, and
|
|
137
|
+
* asking about the field here would mark none of them on the way back down.
|
|
138
|
+
*/
|
|
139
|
+
export function isLevelAtSiteDatum(nodes, levelId) {
|
|
140
|
+
return levelDatum(nodes, levelId) !== null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The lift, in level-local metres, that the sculpted ground provides to a node on
|
|
144
|
+
* `levelId` at level-local `x`/`z`. Null when terrain does not support this node —
|
|
145
|
+
* no sculpted ground, an unresolvable level, or a storey whose floor is not at
|
|
146
|
+
* grade.
|
|
147
|
+
*
|
|
148
|
+
* Null rather than 0 so callers keep their existing flat-ground path verbatim.
|
|
149
|
+
* That is what makes this change invisible to every scene that never touched
|
|
150
|
+
* terrain, which is nearly all of them.
|
|
151
|
+
*/
|
|
152
|
+
export function terrainSupportLift(nodes, levelId, x, z) {
|
|
153
|
+
const site = siteOf(nodes);
|
|
154
|
+
const field = site ? terrainFieldOf(site) : null;
|
|
155
|
+
if (!field)
|
|
156
|
+
return null;
|
|
157
|
+
const datum = levelDatum(nodes, levelId);
|
|
158
|
+
if (!datum)
|
|
159
|
+
return null;
|
|
160
|
+
const [worldX, worldZ] = levelLocalToWorldXZ(datum.building, x, z);
|
|
161
|
+
// The lift is measured from the storey's own base, so a building sited on a
|
|
162
|
+
// datum other than 0 does not double-count it.
|
|
163
|
+
return heightAt(field, worldX, worldZ) - datum.baseWorldY;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* **The level base, in level-local metres, at level-local `x`/`z`** — the surface
|
|
167
|
+
* a node rests on when nothing built is under it. Sculpted ground where terrain
|
|
168
|
+
* supports this storey, `0` everywhere else.
|
|
169
|
+
*
|
|
170
|
+
* This is the one function every "nothing is under me, so I'm at zero" site in
|
|
171
|
+
* the codebase should call, and the reason it exists separately from
|
|
172
|
+
* {@link terrainSupportLift}: that function answers *"is there terrain here"*
|
|
173
|
+
* (nullable, so a caller can branch), while this one answers *"how high is the
|
|
174
|
+
* floor of the world here"* (total, so a caller does not have to know terrain
|
|
175
|
+
* exists). Spelling the second question `terrainSupportLift(…) ?? 0` at each
|
|
176
|
+
* site is what made terrain opt-in per kind — every new consumer had to remember
|
|
177
|
+
* to ask, and the ones that forgot silently assumed the plane `y = 0`. Kinds
|
|
178
|
+
* inherit terrain by resolving their base through here instead of hardcoding a
|
|
179
|
+
* zero; nothing has to be registered for that to work.
|
|
180
|
+
*
|
|
181
|
+
* Callers that must distinguish "flat ground" from "a built surface flush with
|
|
182
|
+
* the storey base" still need {@link terrainSupportLift}'s null — both read `0`
|
|
183
|
+
* here and only the first follows a hillside.
|
|
184
|
+
*/
|
|
185
|
+
export function levelBaseElevationAt(nodes, levelId, x, z) {
|
|
186
|
+
return terrainSupportLift(nodes, levelId, x, z) ?? 0;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Kinds whose geometry builder has actually asked for the level base, learned at
|
|
190
|
+
* runtime from the first build rather than declared.
|
|
191
|
+
*
|
|
192
|
+
* A builder that bakes its vertical origin (`ctx.levelBaseAt`) must be rebuilt
|
|
193
|
+
* when the ground moves, and core cannot see inside a pure function to know
|
|
194
|
+
* which kinds do. The two declarative alternatives both fail the goal: a flag on
|
|
195
|
+
* the definition is another per-kind opt-in — the exact thing that left half the
|
|
196
|
+
* scene flat on a hillside — and over-approximating to "every kind with a
|
|
197
|
+
* geometry builder" would rebuild every cabinet and duct on the ground floor on
|
|
198
|
+
* every dab of a brush stroke.
|
|
199
|
+
*
|
|
200
|
+
* So the question is answered by the call itself: asking for the ground is what
|
|
201
|
+
* enrolls the kind in following it. A plugin inherits terrain by reading
|
|
202
|
+
* `ctx.levelBaseAt` and nothing else — no registration, no capability, no core
|
|
203
|
+
* change. Keyed by node *type*, not id: types are bounded and stable, so the set
|
|
204
|
+
* cannot leak with the scene, and a kind that asked once will ask again on every
|
|
205
|
+
* subsequent build of every instance.
|
|
206
|
+
*
|
|
207
|
+
* Ordering is not a hazard: geometry builds on mount, long before any sculpt can
|
|
208
|
+
* happen, and a node created after a stroke builds against the current field.
|
|
209
|
+
*/
|
|
210
|
+
const levelBaseConsumerKinds = new Set();
|
|
211
|
+
/** Record that `type`'s geometry builder resolved its origin from the ground. */
|
|
212
|
+
export function noteLevelBaseConsumer(type) {
|
|
213
|
+
levelBaseConsumerKinds.add(type);
|
|
214
|
+
}
|
|
215
|
+
/** Whether `type`'s geometry has to be rebuilt when the sculpted ground moves. */
|
|
216
|
+
export function isLevelBaseConsumer(type) {
|
|
217
|
+
return levelBaseConsumerKinds.has(type);
|
|
218
|
+
}
|
|
@@ -17,5 +17,5 @@ export type ZoneQuantityReport = {
|
|
|
17
17
|
floorSurface: ZoneQuantityValue;
|
|
18
18
|
volume: ZoneQuantityValue;
|
|
19
19
|
};
|
|
20
|
-
export declare function deriveZoneQuantityReport(zone: ZoneNode, sceneNodes: Record<string, AnyNode
|
|
20
|
+
export declare function deriveZoneQuantityReport(zone: ZoneNode, sceneNodes: Readonly<Record<string, AnyNode>>): ZoneQuantityReport;
|
|
21
21
|
//# sourceMappingURL=zone-quantities.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zone-quantities.d.ts","sourceRoot":"","sources":["../../src/lib/zone-quantities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAmC,QAAQ,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"zone-quantities.d.ts","sourceRoot":"","sources":["../../src/lib/zone-quantities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAmC,QAAQ,EAAE,MAAM,WAAW,CAAA;AAYnF,MAAM,MAAM,iBAAiB,GACzB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACrD;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAE7C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,cAAc,EAAE,WAAW,GAAG,eAAe,CAAA;IAC7C,aAAa,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,WAAW,EAAE,iBAAiB,CAAA;IAC9B,YAAY,EAAE,iBAAiB,CAAA;IAC/B,MAAM,EAAE,iBAAiB,CAAA;CAC1B,CAAA;AA8bD,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,QAAQ,EACd,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAC5C,kBAAkB,CAmGpB"}
|