@pascal-app/core 0.9.1 → 1.0.0-beta.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/README.md +17 -10
- package/dist/events/bus.d.ts +49 -2
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/hooks/scene-registry/item-clip-registry.d.ts +18 -0
- package/dist/hooks/scene-registry/item-clip-registry.d.ts.map +1 -0
- package/dist/hooks/scene-registry/item-clip-registry.js +10 -0
- package/dist/hooks/scene-registry/scene-registry.d.ts +8 -1
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.js +27 -1
- package/dist/hooks/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 +135 -28
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +427 -271
- 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 +208 -9
- 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 +33 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -10
- package/dist/lib/level-name.d.ts +4 -0
- package/dist/lib/level-name.d.ts.map +1 -0
- package/dist/lib/level-name.js +10 -0
- package/dist/lib/measurement-geometry.d.ts +23 -0
- package/dist/lib/measurement-geometry.d.ts.map +1 -0
- package/dist/lib/measurement-geometry.js +249 -0
- package/dist/lib/polygon-geometry.d.ts +0 -1
- package/dist/lib/polygon-geometry.d.ts.map +1 -1
- package/dist/lib/polygon-geometry.js +0 -18
- package/dist/lib/selection-proxy.d.ts +4 -0
- package/dist/lib/selection-proxy.d.ts.map +1 -0
- package/dist/lib/selection-proxy.js +12 -0
- package/dist/lib/slab-polygon.d.ts +31 -2
- package/dist/lib/slab-polygon.d.ts.map +1 -1
- package/dist/lib/slab-polygon.js +557 -31
- package/dist/lib/slots.d.ts +14 -0
- package/dist/lib/slots.d.ts.map +1 -0
- package/dist/lib/slots.js +26 -0
- package/dist/lib/space-detection.d.ts +40 -6
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +574 -192
- 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 +60 -0
- package/dist/lib/terrain-support.d.ts.map +1 -0
- package/dist/lib/terrain-support.js +150 -0
- package/dist/lib/wall-distance.d.ts +60 -0
- package/dist/lib/wall-distance.d.ts.map +1 -0
- package/dist/lib/wall-distance.js +85 -0
- package/dist/lib/zone-quantities.d.ts +21 -0
- package/dist/lib/zone-quantities.d.ts.map +1 -0
- package/dist/lib/zone-quantities.js +428 -0
- package/dist/material-library.d.ts +28 -1
- package/dist/material-library.d.ts.map +1 -1
- package/dist/material-library.js +2233 -441
- package/dist/registry/handles.d.ts +91 -3
- package/dist/registry/handles.d.ts.map +1 -1
- package/dist/registry/index.d.ts +3 -3
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +1 -1
- package/dist/registry/registry.d.ts +46 -3
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/registry.js +82 -1
- package/dist/registry/subtree.d.ts.map +1 -1
- package/dist/registry/subtree.js +8 -1
- package/dist/registry/types.d.ts +781 -39
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/schema/index.d.ts +27 -8
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +23 -6
- package/dist/schema/material.d.ts +3 -2
- package/dist/schema/material.d.ts.map +1 -1
- package/dist/schema/material.js +7 -5
- package/dist/schema/nodes/box-vent.d.ts +2 -2
- package/dist/schema/nodes/cabinet.d.ts +536 -0
- package/dist/schema/nodes/cabinet.d.ts.map +1 -0
- package/dist/schema/nodes/cabinet.js +139 -0
- package/dist/schema/nodes/ceiling.d.ts +4 -3
- package/dist/schema/nodes/ceiling.d.ts.map +1 -1
- package/dist/schema/nodes/ceiling.js +11 -1
- package/dist/schema/nodes/chimney.d.ts +4 -4
- package/dist/schema/nodes/column.d.ts +4 -2
- package/dist/schema/nodes/column.d.ts.map +1 -1
- package/dist/schema/nodes/column.js +5 -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/cupola.d.ts +2 -2
- package/dist/schema/nodes/door.d.ts +42 -5
- package/dist/schema/nodes/door.d.ts.map +1 -1
- package/dist/schema/nodes/door.js +32 -0
- package/dist/schema/nodes/dormer.d.ts +8 -8
- package/dist/schema/nodes/dormer.js +1 -1
- package/dist/schema/nodes/downspout.d.ts +2 -2
- package/dist/schema/nodes/duct-fitting.d.ts +91 -0
- package/dist/schema/nodes/duct-fitting.d.ts.map +1 -0
- package/dist/schema/nodes/duct-fitting.js +92 -0
- package/dist/schema/nodes/duct-segment.d.ts +61 -0
- package/dist/schema/nodes/duct-segment.d.ts.map +1 -0
- package/dist/schema/nodes/duct-segment.js +73 -0
- package/dist/schema/nodes/duct-terminal.d.ts +58 -0
- package/dist/schema/nodes/duct-terminal.d.ts.map +1 -0
- package/dist/schema/nodes/duct-terminal.js +53 -0
- package/dist/schema/nodes/elevator.d.ts +3 -2
- package/dist/schema/nodes/elevator.d.ts.map +1 -1
- package/dist/schema/nodes/elevator.js +3 -0
- package/dist/schema/nodes/eyebrow-vent.d.ts +2 -2
- package/dist/schema/nodes/fence.d.ts +20 -2
- package/dist/schema/nodes/fence.d.ts.map +1 -1
- package/dist/schema/nodes/fence.js +33 -2
- package/dist/schema/nodes/gutter.d.ts +2 -2
- package/dist/schema/nodes/hvac-equipment.d.ts +64 -0
- package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -0
- package/dist/schema/nodes/hvac-equipment.js +57 -0
- package/dist/schema/nodes/item.d.ts +9 -0
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +24 -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 -31
- package/dist/schema/nodes/lineset.d.ts +42 -0
- package/dist/schema/nodes/lineset.d.ts.map +1 -0
- package/dist/schema/nodes/lineset.js +38 -0
- package/dist/schema/nodes/liquid-line.d.ts +34 -0
- package/dist/schema/nodes/liquid-line.d.ts.map +1 -0
- package/dist/schema/nodes/liquid-line.js +24 -0
- package/dist/schema/nodes/measurement.d.ts +306 -0
- package/dist/schema/nodes/measurement.d.ts.map +1 -0
- package/dist/schema/nodes/measurement.js +87 -0
- package/dist/schema/nodes/pipe-fitting.d.ts +56 -0
- package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -0
- package/dist/schema/nodes/pipe-fitting.js +45 -0
- package/dist/schema/nodes/pipe-segment.d.ts +48 -0
- package/dist/schema/nodes/pipe-segment.d.ts.map +1 -0
- package/dist/schema/nodes/pipe-segment.js +36 -0
- package/dist/schema/nodes/pipe-trap.d.ts +43 -0
- package/dist/schema/nodes/pipe-trap.d.ts.map +1 -0
- package/dist/schema/nodes/pipe-trap.js +36 -0
- package/dist/schema/nodes/ridge-vent.d.ts +18 -3
- package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
- package/dist/schema/nodes/ridge-vent.js +221 -3
- package/dist/schema/nodes/roof-segment-shape.d.ts +90 -0
- package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-shape.js +318 -0
- package/dist/schema/nodes/roof-segment-walls.d.ts +101 -0
- package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-walls.js +330 -0
- package/dist/schema/nodes/roof-segment.d.ts +84 -8
- package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment.js +290 -5
- package/dist/schema/nodes/roof.d.ts +8 -8
- package/dist/schema/nodes/shelf.d.ts +4 -2
- package/dist/schema/nodes/shelf.d.ts.map +1 -1
- package/dist/schema/nodes/shelf.js +8 -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/skylight.d.ts +10 -10
- package/dist/schema/nodes/slab.d.ts +8 -2
- package/dist/schema/nodes/slab.d.ts.map +1 -1
- package/dist/schema/nodes/slab.js +18 -2
- package/dist/schema/nodes/solar-panel.d.ts +4 -4
- 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-segment.d.ts +4 -4
- package/dist/schema/nodes/stair.d.ts +16 -13
- package/dist/schema/nodes/stair.d.ts.map +1 -1
- package/dist/schema/nodes/stair.js +11 -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/turbine-vent.d.ts +2 -2
- package/dist/schema/nodes/wall.d.ts +248 -6
- package/dist/schema/nodes/wall.d.ts.map +1 -1
- package/dist/schema/nodes/wall.js +266 -0
- package/dist/schema/nodes/window.d.ts +40 -3
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/nodes/window.js +30 -0
- package/dist/schema/nodes/zone.d.ts +22 -0
- package/dist/schema/nodes/zone.d.ts.map +1 -1
- package/dist/schema/nodes/zone.js +21 -0
- package/dist/schema/scene-material.d.ts +41 -0
- package/dist/schema/scene-material.d.ts.map +1 -0
- package/dist/schema/scene-material.js +9 -0
- package/dist/schema/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 +1300 -97
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +27 -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/alignment-anchors.d.ts.map +1 -1
- package/dist/services/alignment-anchors.js +30 -1
- package/dist/services/alignment.d.ts +5 -0
- package/dist/services/alignment.d.ts.map +1 -1
- package/dist/services/alignment.js +2 -0
- package/dist/services/hosting.d.ts +8 -0
- package/dist/services/hosting.d.ts.map +1 -1
- package/dist/services/hosting.js +13 -4
- package/dist/services/index.d.ts +9 -2
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +9 -2
- package/dist/services/level-height.d.ts +27 -0
- package/dist/services/level-height.d.ts.map +1 -0
- package/dist/services/level-height.js +94 -0
- package/dist/services/opening-guides.d.ts +141 -0
- package/dist/services/opening-guides.d.ts.map +1 -0
- package/dist/services/opening-guides.js +267 -0
- package/dist/services/port-connectivity.d.ts +115 -0
- package/dist/services/port-connectivity.d.ts.map +1 -0
- package/dist/services/port-connectivity.js +300 -0
- package/dist/services/riser-diagram.d.ts +40 -0
- package/dist/services/riser-diagram.d.ts.map +1 -0
- package/dist/services/riser-diagram.js +110 -0
- package/dist/services/snap.d.ts +9 -1
- package/dist/services/snap.d.ts.map +1 -1
- package/dist/services/snap.js +21 -1
- 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/services/system-graph.d.ts +30 -0
- package/dist/services/system-graph.d.ts.map +1 -0
- package/dist/services/system-graph.js +164 -0
- package/dist/services/validate-dwv.d.ts +32 -0
- package/dist/services/validate-dwv.d.ts.map +1 -0
- package/dist/services/validate-dwv.js +118 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +141 -4
- package/dist/store/history-control.d.ts +31 -0
- package/dist/store/history-control.d.ts.map +1 -1
- package/dist/store/history-control.js +130 -0
- 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 +47 -3
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +992 -55
- 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/fence/fence-centerline.d.ts +12 -0
- package/dist/systems/fence/fence-centerline.d.ts.map +1 -0
- package/dist/systems/fence/fence-centerline.js +37 -0
- package/dist/systems/fence/fence-spline.d.ts +39 -0
- package/dist/systems/fence/fence-spline.d.ts.map +1 -0
- package/dist/systems/fence/fence-spline.js +196 -0
- package/dist/systems/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 +137 -0
- package/dist/systems/slab/slab-support.d.ts.map +1 -0
- package/dist/systems/slab/slab-support.js +557 -0
- package/dist/systems/stair/stair-footprint.d.ts.map +1 -1
- package/dist/systems/stair/stair-footprint.js +21 -11
- package/dist/systems/stair/stair-opening-preview.d.ts +1300 -505
- 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 +19 -33
- 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.d.ts.map +1 -1
- package/dist/systems/wall/wall-mitering.js +44 -8
- 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 +1 -0
- package/dist/utils/clone-scene-graph.d.ts.map +1 -1
- package/dist/utils/clone-scene-graph.js +56 -5
- package/dist/utils/heal-scene-graph.d.ts +18 -0
- package/dist/utils/heal-scene-graph.d.ts.map +1 -0
- package/dist/utils/heal-scene-graph.js +106 -0
- package/dist/validation/validate-build-json.d.ts +3 -0
- package/dist/validation/validate-build-json.d.ts.map +1 -1
- package/dist/validation/validate-build-json.js +90 -5
- package/package.json +4 -4
- 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 -273
- 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 -77
- 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 -183
- 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 -133
- 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/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 -199
- 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/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 -95
- 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/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/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/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 -446
- 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 -59
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire format for a `TerrainField`.
|
|
3
|
+
*
|
|
4
|
+
* The scene graph is persisted as JSON (`projects_models.scene_graph`, a jsonb
|
|
5
|
+
* column), so an `Int16Array` cannot be stored directly — `JSON.stringify` turns
|
|
6
|
+
* a typed array into an object with numeric string keys, which round-trips
|
|
7
|
+
* lossily and costs ~7 bytes per sample. This module is the one place the field
|
|
8
|
+
* crosses that boundary.
|
|
9
|
+
*
|
|
10
|
+
* Base64 of little-endian `Int16` was chosen over the alternatives:
|
|
11
|
+
*
|
|
12
|
+
* - **A plain number array** costs 4-6× the bytes and, worse, re-parses into a
|
|
13
|
+
* `number[]` that every consumer would then have to copy into an `Int16Array`.
|
|
14
|
+
* - **`Array.from(heights)`** has the same problem and silently drops the
|
|
15
|
+
* quantization invariant: nothing stops a hand-edited scene from putting
|
|
16
|
+
* `2.5` in the array, and then `isFlatOver`'s integer equality is meaningless.
|
|
17
|
+
* - **Compression** (RLE, deflate) is deliberately *not* here. Imported DEMs are
|
|
18
|
+
* noisy and compress worst, so a compressed format would fail only on the
|
|
19
|
+
* messiest scenes — the ones most likely to be real. Chunking (`diffToPatches`)
|
|
20
|
+
* is the size mitigation, not compression.
|
|
21
|
+
*
|
|
22
|
+
* Byte order is written explicitly via `DataView` rather than relying on
|
|
23
|
+
* `Int16Array`'s platform order. Every realistic platform is little-endian, but
|
|
24
|
+
* the wire format must not depend on that being true of the machine that saved
|
|
25
|
+
* the scene.
|
|
26
|
+
*/
|
|
27
|
+
import { MAX_TERRAIN_SIDE } from '../schema/terrain';
|
|
28
|
+
const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
29
|
+
function encodeBase64(bytes) {
|
|
30
|
+
let out = '';
|
|
31
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
32
|
+
const b0 = bytes[i] ?? 0;
|
|
33
|
+
const b1 = bytes[i + 1] ?? 0;
|
|
34
|
+
const b2 = bytes[i + 2] ?? 0;
|
|
35
|
+
const remaining = bytes.length - i;
|
|
36
|
+
out += BASE64_ALPHABET[b0 >> 2];
|
|
37
|
+
out += BASE64_ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)];
|
|
38
|
+
out += remaining > 1 ? BASE64_ALPHABET[((b1 & 0x0f) << 2) | (b2 >> 6)] : '=';
|
|
39
|
+
out += remaining > 2 ? BASE64_ALPHABET[b2 & 0x3f] : '=';
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
function decodeBase64(text) {
|
|
44
|
+
// Reverse lookup is built per call rather than at module scope: decode runs
|
|
45
|
+
// once per scene load, and a 64-entry loop is cheaper than the indirection
|
|
46
|
+
// of a module-level Map for a hot path that isn't hot.
|
|
47
|
+
const lookup = new Int8Array(128).fill(-1);
|
|
48
|
+
for (let i = 0; i < BASE64_ALPHABET.length; i++) {
|
|
49
|
+
lookup[BASE64_ALPHABET.charCodeAt(i)] = i;
|
|
50
|
+
}
|
|
51
|
+
let clean = '';
|
|
52
|
+
for (let i = 0; i < text.length; i++) {
|
|
53
|
+
const code = text.charCodeAt(i);
|
|
54
|
+
if (code === 61 /* = */)
|
|
55
|
+
break;
|
|
56
|
+
if (code > 127 || lookup[code] === -1)
|
|
57
|
+
return null;
|
|
58
|
+
clean += text[i];
|
|
59
|
+
}
|
|
60
|
+
if (clean.length % 4 === 1)
|
|
61
|
+
return null;
|
|
62
|
+
const byteLength = Math.floor((clean.length * 3) / 4);
|
|
63
|
+
const bytes = new Uint8Array(byteLength);
|
|
64
|
+
let out = 0;
|
|
65
|
+
for (let i = 0; i < clean.length; i += 4) {
|
|
66
|
+
const c0 = lookup[clean.charCodeAt(i)] ?? 0;
|
|
67
|
+
const c1 = lookup[clean.charCodeAt(i + 1)] ?? 0;
|
|
68
|
+
const c2 = i + 2 < clean.length ? (lookup[clean.charCodeAt(i + 2)] ?? 0) : 0;
|
|
69
|
+
const c3 = i + 3 < clean.length ? (lookup[clean.charCodeAt(i + 3)] ?? 0) : 0;
|
|
70
|
+
if (out < byteLength)
|
|
71
|
+
bytes[out++] = (c0 << 2) | (c1 >> 4);
|
|
72
|
+
if (out < byteLength)
|
|
73
|
+
bytes[out++] = ((c1 & 0x0f) << 4) | (c2 >> 2);
|
|
74
|
+
if (out < byteLength)
|
|
75
|
+
bytes[out++] = ((c2 & 0x03) << 6) | c3;
|
|
76
|
+
}
|
|
77
|
+
return bytes;
|
|
78
|
+
}
|
|
79
|
+
export function encodeTerrainField(field) {
|
|
80
|
+
const bytes = new Uint8Array(field.heights.length * 2);
|
|
81
|
+
const view = new DataView(bytes.buffer);
|
|
82
|
+
for (let i = 0; i < field.heights.length; i++) {
|
|
83
|
+
view.setInt16(i * 2, field.heights[i] ?? 0, true);
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
type: 'heightfield',
|
|
87
|
+
origin: [field.origin[0], field.origin[1]],
|
|
88
|
+
spacing: field.spacing,
|
|
89
|
+
cols: field.cols,
|
|
90
|
+
rows: field.rows,
|
|
91
|
+
step: field.step,
|
|
92
|
+
heights: encodeBase64(bytes),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Rebuild a field from persisted data, or `null` if the data is unusable.
|
|
97
|
+
*
|
|
98
|
+
* Returns `null` rather than throwing, and rather than repairing: a scene whose
|
|
99
|
+
* terrain fails to decode must still load (with flat ground) instead of taking
|
|
100
|
+
* the whole project down, and silently substituting a *different* terrain would
|
|
101
|
+
* be worse than showing none. The caller treats `null` as "this site has no
|
|
102
|
+
* terrain".
|
|
103
|
+
*
|
|
104
|
+
* Metadata and payload length are strict. Repairing a truncated payload or
|
|
105
|
+
* inventing missing metadata would silently substitute a different surface.
|
|
106
|
+
*/
|
|
107
|
+
export function decodeTerrainField(data) {
|
|
108
|
+
if (!data || typeof data !== 'object')
|
|
109
|
+
return null;
|
|
110
|
+
const d = data;
|
|
111
|
+
if (d.type !== 'heightfield')
|
|
112
|
+
return null;
|
|
113
|
+
if (!Number.isInteger(d.cols) || !Number.isInteger(d.rows))
|
|
114
|
+
return null;
|
|
115
|
+
const cols = d.cols;
|
|
116
|
+
const rows = d.rows;
|
|
117
|
+
if (cols < 1 || rows < 1)
|
|
118
|
+
return null;
|
|
119
|
+
if (cols > MAX_TERRAIN_SIDE || rows > MAX_TERRAIN_SIDE)
|
|
120
|
+
return null;
|
|
121
|
+
if (typeof d.heights !== 'string')
|
|
122
|
+
return null;
|
|
123
|
+
if (!Number.isFinite(d.spacing) || d.spacing <= 0)
|
|
124
|
+
return null;
|
|
125
|
+
if (!Number.isFinite(d.step) || d.step <= 0)
|
|
126
|
+
return null;
|
|
127
|
+
if (!Array.isArray(d.origin) ||
|
|
128
|
+
d.origin.length !== 2 ||
|
|
129
|
+
!Number.isFinite(d.origin[0]) ||
|
|
130
|
+
!Number.isFinite(d.origin[1])) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const bytes = decodeBase64(d.heights);
|
|
134
|
+
if (!bytes)
|
|
135
|
+
return null;
|
|
136
|
+
if (encodeBase64(bytes) !== d.heights)
|
|
137
|
+
return null;
|
|
138
|
+
if (bytes.byteLength !== cols * rows * 2)
|
|
139
|
+
return null;
|
|
140
|
+
const heights = new Int16Array(cols * rows);
|
|
141
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
142
|
+
for (let i = 0; i < heights.length; i++) {
|
|
143
|
+
heights[i] = view.getInt16(i * 2, true);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
origin: [d.origin[0], d.origin[1]],
|
|
147
|
+
spacing: d.spacing,
|
|
148
|
+
cols,
|
|
149
|
+
rows,
|
|
150
|
+
step: d.step,
|
|
151
|
+
heights,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* True when every sample is at the datum — the test for "this field is not worth
|
|
156
|
+
* persisting".
|
|
157
|
+
*
|
|
158
|
+
* Called before writing to the node so an untouched site keeps `terrain:
|
|
159
|
+
* undefined` instead of carrying ~11 KB of base64 zeroes in every saved scene.
|
|
160
|
+
*/
|
|
161
|
+
export function isDatumField(field) {
|
|
162
|
+
for (let i = 0; i < field.heights.length; i++) {
|
|
163
|
+
if (field.heights[i] !== 0)
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terrain heightfield: one regular grid of quantized heights, and the
|
|
3
|
+
* only place terrain geometry is read from.
|
|
4
|
+
*
|
|
5
|
+
* Two invariants carry the whole design:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Quantized to `Int16` multiples of `step`.** Heights are integers, so
|
|
8
|
+
* "is this ground flat?" is exact integer equality rather than an epsilon
|
|
9
|
+
* comparison. That is what lets the support election treat terrain as one
|
|
10
|
+
* discrete candidate (see `slab-support.ts`) instead of a float soup that
|
|
11
|
+
* never ties. `step` is metres per unit; at 0.01 the Int16 range covers
|
|
12
|
+
* ±327 m of relief, far past any building site.
|
|
13
|
+
* 2. **Every read goes through `heightAt`.** Consumers never index `heights`
|
|
14
|
+
* directly. One authority means the mesh, the raycast, the placement
|
|
15
|
+
* predicate, and the tests cannot disagree about where the ground is.
|
|
16
|
+
*
|
|
17
|
+
* Pure data — no Three.js, per the core layer rule. The mesh built from this
|
|
18
|
+
* lives in `@pascal-app/nodes`.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* A regular grid of heights over the XZ plane.
|
|
22
|
+
*
|
|
23
|
+
* `origin` is the world XZ of sample [0,0]; sample [c,r] sits at
|
|
24
|
+
* `(origin[0] + c * spacing, origin[1] + r * spacing)`. Row-major, so index
|
|
25
|
+
* `r * cols + c`.
|
|
26
|
+
*
|
|
27
|
+
* Sized `2ⁿ+1` per side by convention (33, 65, 129, …) so a future LOD halving
|
|
28
|
+
* lands on existing samples and shares edge vertices with its neighbour rather
|
|
29
|
+
* than interpolating a seam.
|
|
30
|
+
*/
|
|
31
|
+
export type TerrainField = {
|
|
32
|
+
/** World XZ of sample [0,0]. */
|
|
33
|
+
readonly origin: readonly [number, number];
|
|
34
|
+
/** Metres between adjacent samples. */
|
|
35
|
+
readonly spacing: number;
|
|
36
|
+
readonly cols: number;
|
|
37
|
+
readonly rows: number;
|
|
38
|
+
/** Metres per `heights` unit — the quantization ladder. */
|
|
39
|
+
readonly step: number;
|
|
40
|
+
/** Row-major quantized heights, length `cols * rows`. */
|
|
41
|
+
readonly heights: Int16Array;
|
|
42
|
+
};
|
|
43
|
+
/** A rectangular sub-block of samples — the unit of every terrain write. */
|
|
44
|
+
export type HeightPatch = {
|
|
45
|
+
readonly col0: number;
|
|
46
|
+
readonly row0: number;
|
|
47
|
+
readonly cols: number;
|
|
48
|
+
readonly rows: number;
|
|
49
|
+
/** Row-major quantized heights, length `cols * rows`. */
|
|
50
|
+
readonly heights: Int16Array;
|
|
51
|
+
};
|
|
52
|
+
export declare const DEFAULT_TERRAIN_STEP = 0.01;
|
|
53
|
+
export declare const DEFAULT_TERRAIN_SPACING = 0.5;
|
|
54
|
+
export declare function createTerrainField(options?: {
|
|
55
|
+
origin?: readonly [number, number];
|
|
56
|
+
spacing?: number;
|
|
57
|
+
cols?: number;
|
|
58
|
+
rows?: number;
|
|
59
|
+
step?: number;
|
|
60
|
+
}): TerrainField;
|
|
61
|
+
/** Quantize a metre height onto the field's ladder, clamped to Int16. */
|
|
62
|
+
export declare function quantize(field: TerrainField, metres: number): number;
|
|
63
|
+
/** Sample [c,r] in metres. Out-of-range indices clamp to the edge, so a
|
|
64
|
+
* consumer reading just past the boundary gets the border height rather than
|
|
65
|
+
* a hole — the same clamp-don't-ask rule the vertical model uses. */
|
|
66
|
+
export declare function heightAtSample(field: TerrainField, col: number, row: number): number;
|
|
67
|
+
/**
|
|
68
|
+
* The ground height in metres at a world XZ, on the rendered triangle plane.
|
|
69
|
+
*
|
|
70
|
+
* This is the single read authority named in the plan. Each cell uses the same
|
|
71
|
+
* `(c+1,r)`–`(c,r+1)` diagonal as `nodes/src/site/terrain-geometry.ts`, so
|
|
72
|
+
* placement, picking, collision, draping, and pixels all answer the same height.
|
|
73
|
+
*/
|
|
74
|
+
export declare function heightAt(field: TerrainField, x: number, z: number): number;
|
|
75
|
+
/**
|
|
76
|
+
* Compatibility name for callers that explicitly describe a rendered surface.
|
|
77
|
+
* There is intentionally no second interpolation model: `heightAt` is the
|
|
78
|
+
* rendered triangle plane.
|
|
79
|
+
*/
|
|
80
|
+
export declare function surfaceHeightAt(field: TerrainField, x: number, z: number): number;
|
|
81
|
+
/**
|
|
82
|
+
* Surface normal at a world XZ, from analytic central differences.
|
|
83
|
+
*
|
|
84
|
+
* Central differences rather than per-triangle face normals: the field is the
|
|
85
|
+
* authority, so the normal is a property of the *field* and stays continuous
|
|
86
|
+
* across triangle boundaries. Returns a unit vector, +Y up.
|
|
87
|
+
*/
|
|
88
|
+
export declare function normalAt(field: TerrainField, x: number, z: number): [number, number, number];
|
|
89
|
+
/** Slope in radians from horizontal at a world XZ. 0 = flat, π/2 = vertical. */
|
|
90
|
+
export declare function slopeAt(field: TerrainField, x: number, z: number): number;
|
|
91
|
+
/**
|
|
92
|
+
* True when every sample under the world-XZ rect is at the identical quantized
|
|
93
|
+
* height — the placement predicate for kinds that need level ground.
|
|
94
|
+
*
|
|
95
|
+
* Exact integer equality, which is only meaningful because heights are
|
|
96
|
+
* quantized (invariant 1). A float field would need a tolerance here, and that
|
|
97
|
+
* tolerance would then have to agree with the support election's own epsilon —
|
|
98
|
+
* two tunables that could drift apart. Instead there is one ladder and no
|
|
99
|
+
* tolerance.
|
|
100
|
+
*/
|
|
101
|
+
export declare function isFlatOver(field: TerrainField, minX: number, minZ: number, maxX: number, maxZ: number): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* The one write API. Every terrain mutation — brush stroke, DEM import, scan
|
|
104
|
+
* conversion, preset, MCP tool — lands here.
|
|
105
|
+
*
|
|
106
|
+
* Returns a new field with a new `heights` buffer, so a stroke's before/after
|
|
107
|
+
* pair can be diffed for undo and for `diffToPatches`. Patch samples outside
|
|
108
|
+
* the field are ignored rather than throwing: an imported DEM is allowed to
|
|
109
|
+
* overhang the site, and clipping is the sane response.
|
|
110
|
+
*/
|
|
111
|
+
export declare function applyHeightPatch(field: TerrainField, patch: HeightPatch): TerrainField;
|
|
112
|
+
/** The world-XZ sample range a rect covers, clamped to the field. Shared by
|
|
113
|
+
* the brushes so every tool agrees which samples a footprint touches. */
|
|
114
|
+
export declare function sampleRangeOver(field: TerrainField, minX: number, minZ: number, maxX: number, maxZ: number): {
|
|
115
|
+
col0: number;
|
|
116
|
+
row0: number;
|
|
117
|
+
col1: number;
|
|
118
|
+
row1: number;
|
|
119
|
+
} | null;
|
|
120
|
+
/**
|
|
121
|
+
* The only brush in the first slice: set every sample under a world-XZ rect to
|
|
122
|
+
* one absolute height.
|
|
123
|
+
*
|
|
124
|
+
* Absolute-height flatten before raise/lower is deliberate. A raise/lower brush
|
|
125
|
+
* answers "make this taller", which the user cannot aim; flatten-to-height
|
|
126
|
+
* answers "put the ground at 2.5 m here", which is the actual task when siting
|
|
127
|
+
* a building — and it is the one brush whose result is predictable enough to
|
|
128
|
+
* test. Returns null when the rect misses the field entirely.
|
|
129
|
+
*/
|
|
130
|
+
export declare function flattenPatch(field: TerrainField, rect: {
|
|
131
|
+
minX: number;
|
|
132
|
+
minZ: number;
|
|
133
|
+
maxX: number;
|
|
134
|
+
maxZ: number;
|
|
135
|
+
}, metres: number): HeightPatch | null;
|
|
136
|
+
/**
|
|
137
|
+
* Split the difference between two fields into patches that each stay under
|
|
138
|
+
* `maxBytes` when serialized.
|
|
139
|
+
*
|
|
140
|
+
* This exists because scene operations are capped at
|
|
141
|
+
* `MAX_SCENE_OPERATION_BYTES` (64 KiB) and each mutation carries both `from`
|
|
142
|
+
* and `to` — so a whole-field write is over budget the moment the grid gets
|
|
143
|
+
* interesting. Rows are the split unit: a patch is always whole rows, which
|
|
144
|
+
* keeps reassembly trivial and means a single-row edit costs one row.
|
|
145
|
+
*
|
|
146
|
+
* Returns the minimal dirty row span, so a small brush stroke on a large field
|
|
147
|
+
* produces a small patch rather than a full-field rewrite.
|
|
148
|
+
*/
|
|
149
|
+
export declare function diffToPatches(before: TerrainField, after: TerrainField, maxBytes: number): HeightPatch[];
|
|
150
|
+
//# sourceMappingURL=terrain-field.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terrain-field.d.ts","sourceRoot":"","sources":["../../src/lib/terrain-field.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,gCAAgC;IAChC,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC1C,uCAAuC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;CAC7B,CAAA;AAED,4EAA4E;AAC5E,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;CAC7B,CAAA;AAED,eAAO,MAAM,oBAAoB,OAAO,CAAA;AACxC,eAAO,MAAM,uBAAuB,MAAM,CAAA;AAE1C,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE;IAC3C,MAAM,CAAC,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,GAAG,YAAY,CAWf;AAED,yEAAyE;AACzE,wBAAgB,QAAQ,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAIpE;AAED;;qEAEqE;AACrE,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAIpF;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAiB1E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAEjF;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAO5F;AAED,gFAAgF;AAChF,wBAAgB,OAAO,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAKzE;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,YAAY,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAeT;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,GAAG,YAAY,CActF;AAED;yEACyE;AACzE,wBAAgB,eAAe,CAC7B,KAAK,EAAE,YAAY,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAOnE;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,YAAY,EACnB,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAChE,MAAM,EAAE,MAAM,GACb,WAAW,GAAG,IAAI,CASpB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,YAAY,EACnB,QAAQ,EAAE,MAAM,GACf,WAAW,EAAE,CAyBf"}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terrain heightfield: one regular grid of quantized heights, and the
|
|
3
|
+
* only place terrain geometry is read from.
|
|
4
|
+
*
|
|
5
|
+
* Two invariants carry the whole design:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Quantized to `Int16` multiples of `step`.** Heights are integers, so
|
|
8
|
+
* "is this ground flat?" is exact integer equality rather than an epsilon
|
|
9
|
+
* comparison. That is what lets the support election treat terrain as one
|
|
10
|
+
* discrete candidate (see `slab-support.ts`) instead of a float soup that
|
|
11
|
+
* never ties. `step` is metres per unit; at 0.01 the Int16 range covers
|
|
12
|
+
* ±327 m of relief, far past any building site.
|
|
13
|
+
* 2. **Every read goes through `heightAt`.** Consumers never index `heights`
|
|
14
|
+
* directly. One authority means the mesh, the raycast, the placement
|
|
15
|
+
* predicate, and the tests cannot disagree about where the ground is.
|
|
16
|
+
*
|
|
17
|
+
* Pure data — no Three.js, per the core layer rule. The mesh built from this
|
|
18
|
+
* lives in `@pascal-app/nodes`.
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_TERRAIN_STEP = 0.01;
|
|
21
|
+
export const DEFAULT_TERRAIN_SPACING = 0.5;
|
|
22
|
+
export function createTerrainField(options) {
|
|
23
|
+
const cols = options?.cols ?? 65;
|
|
24
|
+
const rows = options?.rows ?? 65;
|
|
25
|
+
return {
|
|
26
|
+
origin: options?.origin ?? [0, 0],
|
|
27
|
+
spacing: options?.spacing ?? DEFAULT_TERRAIN_SPACING,
|
|
28
|
+
cols,
|
|
29
|
+
rows,
|
|
30
|
+
step: options?.step ?? DEFAULT_TERRAIN_STEP,
|
|
31
|
+
heights: new Int16Array(cols * rows),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Quantize a metre height onto the field's ladder, clamped to Int16. */
|
|
35
|
+
export function quantize(field, metres) {
|
|
36
|
+
if (!Number.isFinite(metres))
|
|
37
|
+
return 0;
|
|
38
|
+
const raw = Math.round(metres / field.step);
|
|
39
|
+
return Math.max(-32768, Math.min(32767, raw));
|
|
40
|
+
}
|
|
41
|
+
/** Sample [c,r] in metres. Out-of-range indices clamp to the edge, so a
|
|
42
|
+
* consumer reading just past the boundary gets the border height rather than
|
|
43
|
+
* a hole — the same clamp-don't-ask rule the vertical model uses. */
|
|
44
|
+
export function heightAtSample(field, col, row) {
|
|
45
|
+
const c = Math.max(0, Math.min(field.cols - 1, Math.trunc(col)));
|
|
46
|
+
const r = Math.max(0, Math.min(field.rows - 1, Math.trunc(row)));
|
|
47
|
+
return (field.heights[r * field.cols + c] ?? 0) * field.step;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The ground height in metres at a world XZ, on the rendered triangle plane.
|
|
51
|
+
*
|
|
52
|
+
* This is the single read authority named in the plan. Each cell uses the same
|
|
53
|
+
* `(c+1,r)`–`(c,r+1)` diagonal as `nodes/src/site/terrain-geometry.ts`, so
|
|
54
|
+
* placement, picking, collision, draping, and pixels all answer the same height.
|
|
55
|
+
*/
|
|
56
|
+
export function heightAt(field, x, z) {
|
|
57
|
+
if (field.cols < 2 || field.rows < 2)
|
|
58
|
+
return heightAtSample(field, 0, 0);
|
|
59
|
+
const u = Math.min(field.cols - 1, Math.max(0, (x - field.origin[0]) / field.spacing));
|
|
60
|
+
const v = Math.min(field.rows - 1, Math.max(0, (z - field.origin[1]) / field.spacing));
|
|
61
|
+
const col = Math.min(field.cols - 2, Math.floor(u));
|
|
62
|
+
const row = Math.min(field.rows - 2, Math.floor(v));
|
|
63
|
+
const tc = u - col;
|
|
64
|
+
const tr = v - row;
|
|
65
|
+
const h00 = heightAtSample(field, col, row);
|
|
66
|
+
const h10 = heightAtSample(field, col + 1, row);
|
|
67
|
+
const h01 = heightAtSample(field, col, row + 1);
|
|
68
|
+
const h11 = heightAtSample(field, col + 1, row + 1);
|
|
69
|
+
if (tc + tr <= 1)
|
|
70
|
+
return h00 + (h10 - h00) * tc + (h01 - h00) * tr;
|
|
71
|
+
return h11 + (h01 - h11) * (1 - tc) + (h10 - h11) * (1 - tr);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Compatibility name for callers that explicitly describe a rendered surface.
|
|
75
|
+
* There is intentionally no second interpolation model: `heightAt` is the
|
|
76
|
+
* rendered triangle plane.
|
|
77
|
+
*/
|
|
78
|
+
export function surfaceHeightAt(field, x, z) {
|
|
79
|
+
return heightAt(field, x, z);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Surface normal at a world XZ, from analytic central differences.
|
|
83
|
+
*
|
|
84
|
+
* Central differences rather than per-triangle face normals: the field is the
|
|
85
|
+
* authority, so the normal is a property of the *field* and stays continuous
|
|
86
|
+
* across triangle boundaries. Returns a unit vector, +Y up.
|
|
87
|
+
*/
|
|
88
|
+
export function normalAt(field, x, z) {
|
|
89
|
+
const d = field.spacing;
|
|
90
|
+
const dhdx = (heightAt(field, x + d, z) - heightAt(field, x - d, z)) / (2 * d);
|
|
91
|
+
const dhdz = (heightAt(field, x, z + d) - heightAt(field, x, z - d)) / (2 * d);
|
|
92
|
+
// Gradient (dh/dx, dh/dz) → normal (-dh/dx, 1, -dh/dz), normalized.
|
|
93
|
+
const len = Math.hypot(dhdx, 1, dhdz);
|
|
94
|
+
return [-dhdx / len, 1 / len, -dhdz / len];
|
|
95
|
+
}
|
|
96
|
+
/** Slope in radians from horizontal at a world XZ. 0 = flat, π/2 = vertical. */
|
|
97
|
+
export function slopeAt(field, x, z) {
|
|
98
|
+
const d = field.spacing;
|
|
99
|
+
const dhdx = (heightAt(field, x + d, z) - heightAt(field, x - d, z)) / (2 * d);
|
|
100
|
+
const dhdz = (heightAt(field, x, z + d) - heightAt(field, x, z - d)) / (2 * d);
|
|
101
|
+
return Math.atan(Math.hypot(dhdx, dhdz));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* True when every sample under the world-XZ rect is at the identical quantized
|
|
105
|
+
* height — the placement predicate for kinds that need level ground.
|
|
106
|
+
*
|
|
107
|
+
* Exact integer equality, which is only meaningful because heights are
|
|
108
|
+
* quantized (invariant 1). A float field would need a tolerance here, and that
|
|
109
|
+
* tolerance would then have to agree with the support election's own epsilon —
|
|
110
|
+
* two tunables that could drift apart. Instead there is one ladder and no
|
|
111
|
+
* tolerance.
|
|
112
|
+
*/
|
|
113
|
+
export function isFlatOver(field, minX, minZ, maxX, maxZ) {
|
|
114
|
+
const c0 = Math.max(0, Math.floor((minX - field.origin[0]) / field.spacing));
|
|
115
|
+
const r0 = Math.max(0, Math.floor((minZ - field.origin[1]) / field.spacing));
|
|
116
|
+
const c1 = Math.min(field.cols - 1, Math.ceil((maxX - field.origin[0]) / field.spacing));
|
|
117
|
+
const r1 = Math.min(field.rows - 1, Math.ceil((maxZ - field.origin[1]) / field.spacing));
|
|
118
|
+
if (c1 < c0 || r1 < r0)
|
|
119
|
+
return true;
|
|
120
|
+
const first = field.heights[r0 * field.cols + c0] ?? 0;
|
|
121
|
+
for (let r = r0; r <= r1; r++) {
|
|
122
|
+
const rowBase = r * field.cols;
|
|
123
|
+
for (let c = c0; c <= c1; c++) {
|
|
124
|
+
if ((field.heights[rowBase + c] ?? 0) !== first)
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The one write API. Every terrain mutation — brush stroke, DEM import, scan
|
|
132
|
+
* conversion, preset, MCP tool — lands here.
|
|
133
|
+
*
|
|
134
|
+
* Returns a new field with a new `heights` buffer, so a stroke's before/after
|
|
135
|
+
* pair can be diffed for undo and for `diffToPatches`. Patch samples outside
|
|
136
|
+
* the field are ignored rather than throwing: an imported DEM is allowed to
|
|
137
|
+
* overhang the site, and clipping is the sane response.
|
|
138
|
+
*/
|
|
139
|
+
export function applyHeightPatch(field, patch) {
|
|
140
|
+
const heights = new Int16Array(field.heights);
|
|
141
|
+
for (let pr = 0; pr < patch.rows; pr++) {
|
|
142
|
+
const row = patch.row0 + pr;
|
|
143
|
+
if (row < 0 || row >= field.rows)
|
|
144
|
+
continue;
|
|
145
|
+
const destBase = row * field.cols;
|
|
146
|
+
const srcBase = pr * patch.cols;
|
|
147
|
+
for (let pc = 0; pc < patch.cols; pc++) {
|
|
148
|
+
const col = patch.col0 + pc;
|
|
149
|
+
if (col < 0 || col >= field.cols)
|
|
150
|
+
continue;
|
|
151
|
+
heights[destBase + col] = patch.heights[srcBase + pc] ?? 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { ...field, heights };
|
|
155
|
+
}
|
|
156
|
+
/** The world-XZ sample range a rect covers, clamped to the field. Shared by
|
|
157
|
+
* the brushes so every tool agrees which samples a footprint touches. */
|
|
158
|
+
export function sampleRangeOver(field, minX, minZ, maxX, maxZ) {
|
|
159
|
+
const col0 = Math.max(0, Math.floor((minX - field.origin[0]) / field.spacing));
|
|
160
|
+
const row0 = Math.max(0, Math.floor((minZ - field.origin[1]) / field.spacing));
|
|
161
|
+
const col1 = Math.min(field.cols - 1, Math.ceil((maxX - field.origin[0]) / field.spacing));
|
|
162
|
+
const row1 = Math.min(field.rows - 1, Math.ceil((maxZ - field.origin[1]) / field.spacing));
|
|
163
|
+
if (col1 < col0 || row1 < row0)
|
|
164
|
+
return null;
|
|
165
|
+
return { col0, row0, col1, row1 };
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The only brush in the first slice: set every sample under a world-XZ rect to
|
|
169
|
+
* one absolute height.
|
|
170
|
+
*
|
|
171
|
+
* Absolute-height flatten before raise/lower is deliberate. A raise/lower brush
|
|
172
|
+
* answers "make this taller", which the user cannot aim; flatten-to-height
|
|
173
|
+
* answers "put the ground at 2.5 m here", which is the actual task when siting
|
|
174
|
+
* a building — and it is the one brush whose result is predictable enough to
|
|
175
|
+
* test. Returns null when the rect misses the field entirely.
|
|
176
|
+
*/
|
|
177
|
+
export function flattenPatch(field, rect, metres) {
|
|
178
|
+
const range = sampleRangeOver(field, rect.minX, rect.minZ, rect.maxX, rect.maxZ);
|
|
179
|
+
if (!range)
|
|
180
|
+
return null;
|
|
181
|
+
const cols = range.col1 - range.col0 + 1;
|
|
182
|
+
const rows = range.row1 - range.row0 + 1;
|
|
183
|
+
const value = quantize(field, metres);
|
|
184
|
+
const heights = new Int16Array(cols * rows);
|
|
185
|
+
heights.fill(value);
|
|
186
|
+
return { col0: range.col0, row0: range.row0, cols, rows, heights };
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Split the difference between two fields into patches that each stay under
|
|
190
|
+
* `maxBytes` when serialized.
|
|
191
|
+
*
|
|
192
|
+
* This exists because scene operations are capped at
|
|
193
|
+
* `MAX_SCENE_OPERATION_BYTES` (64 KiB) and each mutation carries both `from`
|
|
194
|
+
* and `to` — so a whole-field write is over budget the moment the grid gets
|
|
195
|
+
* interesting. Rows are the split unit: a patch is always whole rows, which
|
|
196
|
+
* keeps reassembly trivial and means a single-row edit costs one row.
|
|
197
|
+
*
|
|
198
|
+
* Returns the minimal dirty row span, so a small brush stroke on a large field
|
|
199
|
+
* produces a small patch rather than a full-field rewrite.
|
|
200
|
+
*/
|
|
201
|
+
export function diffToPatches(before, after, maxBytes) {
|
|
202
|
+
if (before.cols !== after.cols || before.rows !== after.rows) {
|
|
203
|
+
// Shape changed — the whole field is dirty; chunk it wholesale.
|
|
204
|
+
return chunkRows(after, 0, after.rows - 1, maxBytes);
|
|
205
|
+
}
|
|
206
|
+
const patches = [];
|
|
207
|
+
let runStart = -1;
|
|
208
|
+
for (let r = 0; r < after.rows; r++) {
|
|
209
|
+
const base = r * after.cols;
|
|
210
|
+
let dirty = false;
|
|
211
|
+
for (let c = 0; c < after.cols; c++) {
|
|
212
|
+
if (before.heights[base + c] !== after.heights[base + c]) {
|
|
213
|
+
dirty = true;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (dirty && runStart < 0)
|
|
218
|
+
runStart = r;
|
|
219
|
+
if (!dirty && runStart >= 0) {
|
|
220
|
+
patches.push(...chunkRows(after, runStart, r - 1, maxBytes));
|
|
221
|
+
runStart = -1;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (runStart >= 0)
|
|
225
|
+
patches.push(...chunkRows(after, runStart, after.rows - 1, maxBytes));
|
|
226
|
+
return patches;
|
|
227
|
+
}
|
|
228
|
+
/** Slice a whole-row span into patches that each fit `maxBytes`. */
|
|
229
|
+
function chunkRows(field, firstRow, lastRow, maxBytes) {
|
|
230
|
+
// 2 bytes per Int16 sample, and the transport carries `from` + `to`, so a
|
|
231
|
+
// row costs twice its raw size. Halving the budget up front keeps the caller
|
|
232
|
+
// from having to know that.
|
|
233
|
+
const bytesPerRow = field.cols * 2 * 2;
|
|
234
|
+
const rowsPerChunk = Math.max(1, Math.floor(maxBytes / Math.max(1, bytesPerRow)));
|
|
235
|
+
const patches = [];
|
|
236
|
+
for (let start = firstRow; start <= lastRow; start += rowsPerChunk) {
|
|
237
|
+
const end = Math.min(lastRow, start + rowsPerChunk - 1);
|
|
238
|
+
const rows = end - start + 1;
|
|
239
|
+
patches.push({
|
|
240
|
+
col0: 0,
|
|
241
|
+
row0: start,
|
|
242
|
+
cols: field.cols,
|
|
243
|
+
rows,
|
|
244
|
+
heights: field.heights.slice(start * field.cols, (end + 1) * field.cols),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return patches;
|
|
248
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ray → terrain intersection.
|
|
3
|
+
*
|
|
4
|
+
* This is the function that replaces `t = -oy / dy` (the flat-ground plane
|
|
5
|
+
* solve) everywhere the editor asks "where on the ground is the pointer?".
|
|
6
|
+
* It lives in `core` with no Three.js so the 2D view, the tools, the placement
|
|
7
|
+
* coordinator, and the tests all call the same code — the failure mode this
|
|
8
|
+
* avoids is each surface having its own idea of where the ground is, which on
|
|
9
|
+
* flat ground is invisible and on a slope is a perspective-skewed offset that
|
|
10
|
+
* only some tools correct for.
|
|
11
|
+
*
|
|
12
|
+
* Method: march the ray in `spacing`-sized steps, watching the sign of
|
|
13
|
+
* `rayY - terrainY`, then bisect the bracketing interval. Marching (rather than
|
|
14
|
+
* per-triangle intersection over the whole grid) is what keeps this O(path
|
|
15
|
+
* length) instead of O(samples) — a 129² field is 32 768 triangles, and testing
|
|
16
|
+
* all of them per pointer-move is not affordable at 50 FPS.
|
|
17
|
+
*
|
|
18
|
+
* The bisection is exact to `spacing / 2^ITERATIONS`; at 0.5 m spacing and 20
|
|
19
|
+
* iterations that is under half a micrometre, i.e. far below the quantization
|
|
20
|
+
* ladder, so the returned point is on the same rendered triangle plane
|
|
21
|
+
* `heightAt` reports.
|
|
22
|
+
*/
|
|
23
|
+
import { type TerrainField } from './terrain-field';
|
|
24
|
+
export type TerrainHit = {
|
|
25
|
+
/** Distance along the (unnormalized) direction vector. */
|
|
26
|
+
readonly t: number;
|
|
27
|
+
readonly x: number;
|
|
28
|
+
readonly y: number;
|
|
29
|
+
readonly z: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* First intersection of a ray with the terrain surface, or `null` if it misses.
|
|
33
|
+
*
|
|
34
|
+
* `direction` need not be normalized; `t` is in units of `direction`'s length,
|
|
35
|
+
* matching `Ray.at(t)` semantics so a viewer-side caller can pass a three.js ray
|
|
36
|
+
* through unchanged.
|
|
37
|
+
*
|
|
38
|
+
* The march tracks the *sign change* of `rayY - surfaceY` rather than assuming
|
|
39
|
+
* the ray starts above the ground, so a ray fired from inside a hill or from a
|
|
40
|
+
* basement finds the surface it exits through. A ray whose origin is already
|
|
41
|
+
* exactly on the surface returns `t = 0`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function raycastTerrain(field: TerrainField, origin: readonly [number, number, number], direction: readonly [number, number, number]): TerrainHit | null;
|
|
44
|
+
//# sourceMappingURL=terrain-raycast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terrain-raycast.d.ts","sourceRoot":"","sources":["../../src/lib/terrain-raycast.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAY,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE7D,MAAM,MAAM,UAAU,GAAG;IACvB,0DAA0D;IAC1D,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAiHD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACzC,SAAS,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAC3C,UAAU,GAAG,IAAI,CAuDnB"}
|