@pascal-app/core 0.9.2 → 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 +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 +316 -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 +193 -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 +174 -88
- 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/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 +64 -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 +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-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,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":"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"}
|
|
@@ -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"}
|
|
@@ -0,0 +1,188 @@
|
|
|
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 { heightAt } from './terrain-field';
|
|
24
|
+
/** Bisection depth. 20 halvings of one cell is sub-micrometre at any sane spacing. */
|
|
25
|
+
const REFINE_ITERATIONS = 20;
|
|
26
|
+
/**
|
|
27
|
+
* How far to march before giving up, in multiples of `spacing`.
|
|
28
|
+
*
|
|
29
|
+
* A ray aimed just above a distant ridge can travel a long way before it dips
|
|
30
|
+
* below the surface, but an unbounded march is a hang risk if a caller passes a
|
|
31
|
+
* near-horizontal ray. The bound is generous relative to any site: at 0.5 m
|
|
32
|
+
* spacing this is 4 km of travel.
|
|
33
|
+
*/
|
|
34
|
+
const MAX_STEPS = 8192;
|
|
35
|
+
/**
|
|
36
|
+
* World-XZ bounds of the field, in metres. The march is clipped to this so a ray
|
|
37
|
+
* pointing away from the site terminates immediately instead of stepping
|
|
38
|
+
* MAX_STEPS times through empty space.
|
|
39
|
+
*/
|
|
40
|
+
function fieldBounds(field) {
|
|
41
|
+
return {
|
|
42
|
+
minX: field.origin[0],
|
|
43
|
+
minZ: field.origin[1],
|
|
44
|
+
maxX: field.origin[0] + (field.cols - 1) * field.spacing,
|
|
45
|
+
maxZ: field.origin[1] + (field.rows - 1) * field.spacing,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Min/max height in metres, cached per `heights` buffer.
|
|
50
|
+
*
|
|
51
|
+
* The vertical extent is what bounds the march for rays that never leave the
|
|
52
|
+
* field in XZ — a straight-down pointer ray, which is the single most common
|
|
53
|
+
* case, has `dx = dz = 0` and therefore an infinite XZ exit. Without a Y bound
|
|
54
|
+
* such a ray relies on `MAX_STEPS` to terminate, and an *upward* one burns all
|
|
55
|
+
* 8192 steps before returning null.
|
|
56
|
+
*
|
|
57
|
+
* The scan is O(samples), so it is cached on the buffer rather than repeated per
|
|
58
|
+
* ray. `applyHeightPatch` allocates a new `Int16Array` for every mutation, which
|
|
59
|
+
* makes the buffer identity a correct version key: a patched field misses the
|
|
60
|
+
* cache and rescans exactly once, and a stale entry is unreachable. `WeakMap`
|
|
61
|
+
* keeps it from retaining fields past their use.
|
|
62
|
+
*/
|
|
63
|
+
const heightRangeCache = new WeakMap();
|
|
64
|
+
function heightRange(field) {
|
|
65
|
+
const cached = heightRangeCache.get(field.heights);
|
|
66
|
+
if (cached)
|
|
67
|
+
return cached;
|
|
68
|
+
let min = 0;
|
|
69
|
+
let max = 0;
|
|
70
|
+
if (field.heights.length > 0) {
|
|
71
|
+
min = field.heights[0] ?? 0;
|
|
72
|
+
max = min;
|
|
73
|
+
for (let i = 1; i < field.heights.length; i++) {
|
|
74
|
+
const h = field.heights[i] ?? 0;
|
|
75
|
+
if (h < min)
|
|
76
|
+
min = h;
|
|
77
|
+
if (h > max)
|
|
78
|
+
max = h;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const range = { minY: min * field.step, maxY: max * field.step };
|
|
82
|
+
heightRangeCache.set(field.heights, range);
|
|
83
|
+
return range;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Slab-method clip of the ray against the field's bounding box, returning the
|
|
87
|
+
* `[tEnter, tExit]` interval to march. Null when the ray misses the box.
|
|
88
|
+
*
|
|
89
|
+
* All three axes are clipped, not just XZ. The Y slab is what bounds a
|
|
90
|
+
* straight-down ray — `dx = dz = 0` makes both XZ slabs "parallel and inside",
|
|
91
|
+
* so an XZ-only clip returns an infinite exit and the march has to fall back on
|
|
92
|
+
* `MAX_STEPS`. The Y slab is padded because the surface is bilinear between
|
|
93
|
+
* samples and a tangent ray must not be clipped exactly at the extremum.
|
|
94
|
+
*/
|
|
95
|
+
function clipToField(field, origin, direction) {
|
|
96
|
+
const bounds = fieldBounds(field);
|
|
97
|
+
const { minY, maxY } = heightRange(field);
|
|
98
|
+
const pad = field.spacing;
|
|
99
|
+
let tEnter = 0;
|
|
100
|
+
let tExit = Number.POSITIVE_INFINITY;
|
|
101
|
+
const axes = [
|
|
102
|
+
[origin[0], direction[0], bounds.minX, bounds.maxX],
|
|
103
|
+
[origin[1], direction[1], minY - pad, maxY + pad],
|
|
104
|
+
[origin[2], direction[2], bounds.minZ, bounds.maxZ],
|
|
105
|
+
];
|
|
106
|
+
for (const [o, d, lo, hi] of axes) {
|
|
107
|
+
if (Math.abs(d) < 1e-12) {
|
|
108
|
+
// Parallel to this slab: either always inside or never.
|
|
109
|
+
if (o < lo || o > hi)
|
|
110
|
+
return null;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const t1 = (lo - o) / d;
|
|
114
|
+
const t2 = (hi - o) / d;
|
|
115
|
+
tEnter = Math.max(tEnter, Math.min(t1, t2));
|
|
116
|
+
tExit = Math.min(tExit, Math.max(t1, t2));
|
|
117
|
+
if (tEnter > tExit)
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return { tEnter, tExit };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* First intersection of a ray with the terrain surface, or `null` if it misses.
|
|
124
|
+
*
|
|
125
|
+
* `direction` need not be normalized; `t` is in units of `direction`'s length,
|
|
126
|
+
* matching `Ray.at(t)` semantics so a viewer-side caller can pass a three.js ray
|
|
127
|
+
* through unchanged.
|
|
128
|
+
*
|
|
129
|
+
* The march tracks the *sign change* of `rayY - surfaceY` rather than assuming
|
|
130
|
+
* the ray starts above the ground, so a ray fired from inside a hill or from a
|
|
131
|
+
* basement finds the surface it exits through. A ray whose origin is already
|
|
132
|
+
* exactly on the surface returns `t = 0`.
|
|
133
|
+
*/
|
|
134
|
+
export function raycastTerrain(field, origin, direction) {
|
|
135
|
+
const dirLength = Math.hypot(direction[0], direction[1], direction[2]);
|
|
136
|
+
if (dirLength < 1e-12)
|
|
137
|
+
return null;
|
|
138
|
+
const clip = clipToField(field, origin, direction);
|
|
139
|
+
if (!clip)
|
|
140
|
+
return null;
|
|
141
|
+
const { tEnter, tExit } = clip;
|
|
142
|
+
// Step in world-space multiples of one cell, converted into t units. Half a
|
|
143
|
+
// cell, not a full one, so a ray crossing a single-cell spike cannot step over
|
|
144
|
+
// its peak and report the ground beyond it.
|
|
145
|
+
const stepT = field.spacing / 2 / dirLength;
|
|
146
|
+
const at = (t) => ({
|
|
147
|
+
x: origin[0] + direction[0] * t,
|
|
148
|
+
y: origin[1] + direction[1] * t,
|
|
149
|
+
z: origin[2] + direction[2] * t,
|
|
150
|
+
});
|
|
151
|
+
const surfaceGap = (t) => {
|
|
152
|
+
const p = at(t);
|
|
153
|
+
return p.y - heightAt(field, p.x, p.z);
|
|
154
|
+
};
|
|
155
|
+
const hitAt = (t) => {
|
|
156
|
+
const p = at(t);
|
|
157
|
+
return { t, x: p.x, y: heightAt(field, p.x, p.z), z: p.z };
|
|
158
|
+
};
|
|
159
|
+
let prevT = tEnter;
|
|
160
|
+
let prevGap = surfaceGap(tEnter);
|
|
161
|
+
// Entering exactly on the surface counts as a hit — a pointer ray grazing the
|
|
162
|
+
// ground plane must not fall through to the caller's flat-plane fallback.
|
|
163
|
+
if (prevGap === 0)
|
|
164
|
+
return hitAt(tEnter);
|
|
165
|
+
const startedAbove = prevGap > 0;
|
|
166
|
+
for (let step = 1; step <= MAX_STEPS; step++) {
|
|
167
|
+
const t = Math.min(tExit, tEnter + step * stepT);
|
|
168
|
+
const gap = surfaceGap(t);
|
|
169
|
+
// A crossing in either direction is a surface hit.
|
|
170
|
+
if (gap === 0 || gap > 0 !== startedAbove) {
|
|
171
|
+
let lo = prevT;
|
|
172
|
+
let hi = t;
|
|
173
|
+
for (let i = 0; i < REFINE_ITERATIONS; i++) {
|
|
174
|
+
const mid = (lo + hi) / 2;
|
|
175
|
+
if (surfaceGap(mid) > 0 === startedAbove)
|
|
176
|
+
lo = mid;
|
|
177
|
+
else
|
|
178
|
+
hi = mid;
|
|
179
|
+
}
|
|
180
|
+
return hitAt(hi);
|
|
181
|
+
}
|
|
182
|
+
prevT = t;
|
|
183
|
+
prevGap = gap;
|
|
184
|
+
if (t >= tExit)
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|