@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,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
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scene → `TerrainField`, cached.
|
|
3
|
+
*
|
|
4
|
+
* Every consumer that needs the ground — the mesh builder, the raycast, the
|
|
5
|
+
* placement predicate, the collider, the 2D view — starts here. It exists to
|
|
6
|
+
* solve one problem: the scene stores terrain as base64 (`site.terrain`), and
|
|
7
|
+
* decoding that per frame, or per pointer-move, would be absurd.
|
|
8
|
+
*
|
|
9
|
+
* The cache is a `WeakMap` keyed on the **`TerrainData` object identity**, which
|
|
10
|
+
* is a correct version key because the scene store is immutable-by-convention:
|
|
11
|
+
* `updateNode` produces a new node object with a new `terrain` object, so a
|
|
12
|
+
* changed terrain necessarily misses the cache and a stale entry is unreachable.
|
|
13
|
+
* No invalidation call, no version counter, nothing to forget to bump.
|
|
14
|
+
*
|
|
15
|
+
* `commitTerrainField` closes the loop: it encodes a field *and* primes the cache
|
|
16
|
+
* with the field it just encoded. That is what makes a sculpt stroke free — after
|
|
17
|
+
* the commit, the next read is a cache hit, so a stroke never decodes base64 even
|
|
18
|
+
* once, let alone per dab.
|
|
19
|
+
*/
|
|
20
|
+
import type { SiteNode } from '../schema/nodes/site';
|
|
21
|
+
import type { TerrainData } from '../schema/terrain';
|
|
22
|
+
import { createTerrainField, type TerrainField } from './terrain-field';
|
|
23
|
+
/**
|
|
24
|
+
* The terrain field for a site, or `null` when the site has no sculpted terrain.
|
|
25
|
+
*
|
|
26
|
+
* `null` means flat ground at the datum. It is deliberately not "a flat field":
|
|
27
|
+
* callers use `null` to take their existing flat-ground fast path, which keeps
|
|
28
|
+
* every scene that has never touched terrain — the overwhelming majority —
|
|
29
|
+
* running exactly the code it ran before terrain existed. That property is worth
|
|
30
|
+
* more than the uniformity of always having a field.
|
|
31
|
+
*
|
|
32
|
+
* An in-flight sculpt stroke wins over the persisted data. Resolving that *here*
|
|
33
|
+
* rather than per caller is what makes a stroke consistent: the mesh, the pointer
|
|
34
|
+
* raycast, the placement predicate, the collider, and the 2D view all see the
|
|
35
|
+
* same ground mid-drag without any of them knowing a stroke exists. The
|
|
36
|
+
* alternative — each consumer checking the live store — is how a drag ends up
|
|
37
|
+
* showing a hill the raycast cannot hit.
|
|
38
|
+
*/
|
|
39
|
+
export declare function terrainFieldOf(site: (Pick<SiteNode, 'terrain'> & {
|
|
40
|
+
id?: string;
|
|
41
|
+
}) | null | undefined): TerrainField | null;
|
|
42
|
+
/**
|
|
43
|
+
* The field a site *would* sculpt into: its existing terrain, or a fresh flat
|
|
44
|
+
* field sized to `options`.
|
|
45
|
+
*
|
|
46
|
+
* Separate from `terrainFieldOf` because the read path wants `null` for "no
|
|
47
|
+
* terrain" and the write path wants something to write into. Collapsing them
|
|
48
|
+
* would force every reader to allocate a field it does not need.
|
|
49
|
+
*/
|
|
50
|
+
export declare function terrainFieldForEdit(site: Pick<SiteNode, 'terrain'> | null | undefined, options?: Parameters<typeof createTerrainField>[0]): TerrainField;
|
|
51
|
+
/**
|
|
52
|
+
* Encode a field for persistence and prime the read cache with it.
|
|
53
|
+
*
|
|
54
|
+
* Call this instead of `encodeTerrainField` whenever the result is going into the
|
|
55
|
+
* scene. The returned data is what to write to `site.terrain`; the cache priming
|
|
56
|
+
* means the very next `terrainFieldOf` on the updated node returns this exact
|
|
57
|
+
* field object without decoding.
|
|
58
|
+
*/
|
|
59
|
+
export declare function commitTerrainField(field: TerrainField): TerrainData;
|
|
60
|
+
//# sourceMappingURL=terrain-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terrain-source.d.ts","sourceRoot":"","sources":["../../src/lib/terrain-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAGpD,OAAO,EAAE,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAUvE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,GAAG;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACrE,YAAY,GAAG,IAAI,CAkBrB;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,EAClD,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC,CAAC,GACjD,YAAY,CAEd;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,GAAG,WAAW,CAInE"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scene → `TerrainField`, cached.
|
|
3
|
+
*
|
|
4
|
+
* Every consumer that needs the ground — the mesh builder, the raycast, the
|
|
5
|
+
* placement predicate, the collider, the 2D view — starts here. It exists to
|
|
6
|
+
* solve one problem: the scene stores terrain as base64 (`site.terrain`), and
|
|
7
|
+
* decoding that per frame, or per pointer-move, would be absurd.
|
|
8
|
+
*
|
|
9
|
+
* The cache is a `WeakMap` keyed on the **`TerrainData` object identity**, which
|
|
10
|
+
* is a correct version key because the scene store is immutable-by-convention:
|
|
11
|
+
* `updateNode` produces a new node object with a new `terrain` object, so a
|
|
12
|
+
* changed terrain necessarily misses the cache and a stale entry is unreachable.
|
|
13
|
+
* No invalidation call, no version counter, nothing to forget to bump.
|
|
14
|
+
*
|
|
15
|
+
* `commitTerrainField` closes the loop: it encodes a field *and* primes the cache
|
|
16
|
+
* with the field it just encoded. That is what makes a sculpt stroke free — after
|
|
17
|
+
* the commit, the next read is a cache hit, so a stroke never decodes base64 even
|
|
18
|
+
* once, let alone per dab.
|
|
19
|
+
*/
|
|
20
|
+
import useLiveTerrain from '../store/use-live-terrain';
|
|
21
|
+
import { decodeTerrainField, encodeTerrainField } from './terrain-codec';
|
|
22
|
+
import { createTerrainField } from './terrain-field';
|
|
23
|
+
const fieldCache = new WeakMap();
|
|
24
|
+
/**
|
|
25
|
+
* Sites whose `terrain` failed to decode. Prevents re-attempting a doomed decode
|
|
26
|
+
* on every frame — the corrupt-scene path must be cheap, not just survivable.
|
|
27
|
+
*/
|
|
28
|
+
const failedDecodes = new WeakSet();
|
|
29
|
+
/**
|
|
30
|
+
* The terrain field for a site, or `null` when the site has no sculpted terrain.
|
|
31
|
+
*
|
|
32
|
+
* `null` means flat ground at the datum. It is deliberately not "a flat field":
|
|
33
|
+
* callers use `null` to take their existing flat-ground fast path, which keeps
|
|
34
|
+
* every scene that has never touched terrain — the overwhelming majority —
|
|
35
|
+
* running exactly the code it ran before terrain existed. That property is worth
|
|
36
|
+
* more than the uniformity of always having a field.
|
|
37
|
+
*
|
|
38
|
+
* An in-flight sculpt stroke wins over the persisted data. Resolving that *here*
|
|
39
|
+
* rather than per caller is what makes a stroke consistent: the mesh, the pointer
|
|
40
|
+
* raycast, the placement predicate, the collider, and the 2D view all see the
|
|
41
|
+
* same ground mid-drag without any of them knowing a stroke exists. The
|
|
42
|
+
* alternative — each consumer checking the live store — is how a drag ends up
|
|
43
|
+
* showing a hill the raycast cannot hit.
|
|
44
|
+
*/
|
|
45
|
+
export function terrainFieldOf(site) {
|
|
46
|
+
if (site?.id) {
|
|
47
|
+
const live = useLiveTerrain.getState().fieldOf(site.id);
|
|
48
|
+
if (live)
|
|
49
|
+
return live;
|
|
50
|
+
}
|
|
51
|
+
const data = site?.terrain;
|
|
52
|
+
if (!data)
|
|
53
|
+
return null;
|
|
54
|
+
const cached = fieldCache.get(data);
|
|
55
|
+
if (cached)
|
|
56
|
+
return cached;
|
|
57
|
+
if (failedDecodes.has(data))
|
|
58
|
+
return null;
|
|
59
|
+
const field = decodeTerrainField(data);
|
|
60
|
+
if (!field) {
|
|
61
|
+
failedDecodes.add(data);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
fieldCache.set(data, field);
|
|
65
|
+
return field;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The field a site *would* sculpt into: its existing terrain, or a fresh flat
|
|
69
|
+
* field sized to `options`.
|
|
70
|
+
*
|
|
71
|
+
* Separate from `terrainFieldOf` because the read path wants `null` for "no
|
|
72
|
+
* terrain" and the write path wants something to write into. Collapsing them
|
|
73
|
+
* would force every reader to allocate a field it does not need.
|
|
74
|
+
*/
|
|
75
|
+
export function terrainFieldForEdit(site, options) {
|
|
76
|
+
return terrainFieldOf(site) ?? createTerrainField(options);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Encode a field for persistence and prime the read cache with it.
|
|
80
|
+
*
|
|
81
|
+
* Call this instead of `encodeTerrainField` whenever the result is going into the
|
|
82
|
+
* scene. The returned data is what to write to `site.terrain`; the cache priming
|
|
83
|
+
* means the very next `terrainFieldOf` on the updated node returns this exact
|
|
84
|
+
* field object without decoding.
|
|
85
|
+
*/
|
|
86
|
+
export function commitTerrainField(field) {
|
|
87
|
+
const data = encodeTerrainField(field);
|
|
88
|
+
fieldCache.set(data, field);
|
|
89
|
+
return data;
|
|
90
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terrain as the level base — the third and last place flat ground was assumed.
|
|
3
|
+
*
|
|
4
|
+
* `getFloorPlacedElevation` returns a *lift* added to a node's level-local Y, and
|
|
5
|
+
* two of its `0`-returns mean "supported by the level base". This module is what
|
|
6
|
+
* those two branches call so that "the level base" can mean the sculpted ground
|
|
7
|
+
* instead of the plane `y = 0`. The other `0`-returns are `+0` no-ops for
|
|
8
|
+
* wall/ceiling-attached items, non-`level` parents, and broken graphs; substituting
|
|
9
|
+
* terrain there would lift sconces off walls, so they stay `0` forever.
|
|
10
|
+
*
|
|
11
|
+
* The whole file is pure — nodes in, metres out — which is what lets the stacking
|
|
12
|
+
* matrix be tested per kind without a canvas.
|
|
13
|
+
*/
|
|
14
|
+
import type { AnyNode } from '../schema';
|
|
15
|
+
/**
|
|
16
|
+
* World Y of the site datum — the plane terrain heights are measured from.
|
|
17
|
+
*
|
|
18
|
+
* Terrain vertices are absolute world coordinates (the site group renders at the
|
|
19
|
+
* origin and `terrain-geometry` writes `origin + col * spacing` straight into the
|
|
20
|
+
* buffer), so the datum is a constant rather than a site property. It is named
|
|
21
|
+
* because the *comparison* against it is load-bearing, not the value: it is what
|
|
22
|
+
* separates "the ground" from every other horizontal surface in the scene.
|
|
23
|
+
*/
|
|
24
|
+
export declare const SITE_DATUM_Y = 0;
|
|
25
|
+
/**
|
|
26
|
+
* How close a surface must sit to the datum to count as the ground.
|
|
27
|
+
*
|
|
28
|
+
* Loose enough to absorb float drift through a level's world matrix, tight enough
|
|
29
|
+
* that a ground slab (5 cm) still reads as a built surface rather than as terrain.
|
|
30
|
+
*/
|
|
31
|
+
export declare const SITE_DATUM_EPSILON = 0.0001;
|
|
32
|
+
/** True when a world-space height is the site datum — i.e. the terrain's plane. */
|
|
33
|
+
export declare function isSiteDatum(worldY: number): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Whether the sculpted ground is what a node on `levelId` stands on — i.e.
|
|
36
|
+
* whether moving the terrain moves that node.
|
|
37
|
+
*
|
|
38
|
+
* Split out of `terrainSupportLift` so the invalidation rule that re-elevates
|
|
39
|
+
* nodes after a sculpt (`markTerrainSupportDependents`) tests grade-ness with
|
|
40
|
+
* the *same* predicate the resolver does. Two copies of this test would drift,
|
|
41
|
+
* and the failure is silent: a level the resolver drapes but the dirty rule
|
|
42
|
+
* skips leaves its contents frozen at the height the ground used to be.
|
|
43
|
+
*
|
|
44
|
+
* Deliberately independent of whether a field exists. A stroke that clears the
|
|
45
|
+
* terrain has to re-elevate exactly the nodes a stroke that creates it does, and
|
|
46
|
+
* asking about the field here would mark none of them on the way back down.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isLevelAtSiteDatum(nodes: Record<string, AnyNode>, levelId: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* The lift, in level-local metres, that the sculpted ground provides to a node on
|
|
51
|
+
* `levelId` at level-local `x`/`z`. Null when terrain does not support this node —
|
|
52
|
+
* no sculpted ground, an unresolvable level, or a storey whose floor is not at
|
|
53
|
+
* grade.
|
|
54
|
+
*
|
|
55
|
+
* Null rather than 0 so callers keep their existing flat-ground path verbatim.
|
|
56
|
+
* That is what makes this change invisible to every scene that never touched
|
|
57
|
+
* terrain, which is nearly all of them.
|
|
58
|
+
*/
|
|
59
|
+
export declare function terrainSupportLift(nodes: Record<string, AnyNode>, levelId: string, x: number, z: number): number | null;
|
|
60
|
+
//# sourceMappingURL=terrain-support.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terrain-support.d.ts","sourceRoot":"","sources":["../../src/lib/terrain-support.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAa,MAAM,WAAW,CAAA;AAOnD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,IAAI,CAAA;AAE7B;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,SAAO,CAAA;AAEtC,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;AAqFD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAE3F;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,EACf,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,GACR,MAAM,GAAG,IAAI,CAYf"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terrain as the level base — the third and last place flat ground was assumed.
|
|
3
|
+
*
|
|
4
|
+
* `getFloorPlacedElevation` returns a *lift* added to a node's level-local Y, and
|
|
5
|
+
* two of its `0`-returns mean "supported by the level base". This module is what
|
|
6
|
+
* those two branches call so that "the level base" can mean the sculpted ground
|
|
7
|
+
* instead of the plane `y = 0`. The other `0`-returns are `+0` no-ops for
|
|
8
|
+
* wall/ceiling-attached items, non-`level` parents, and broken graphs; substituting
|
|
9
|
+
* terrain there would lift sconces off walls, so they stay `0` forever.
|
|
10
|
+
*
|
|
11
|
+
* The whole file is pure — nodes in, metres out — which is what lets the stacking
|
|
12
|
+
* matrix be tested per kind without a canvas.
|
|
13
|
+
*/
|
|
14
|
+
import { getLevelElevations } from '../services/storey';
|
|
15
|
+
import { heightAt } from './terrain-field';
|
|
16
|
+
import { terrainFieldOf } from './terrain-source';
|
|
17
|
+
/**
|
|
18
|
+
* World Y of the site datum — the plane terrain heights are measured from.
|
|
19
|
+
*
|
|
20
|
+
* Terrain vertices are absolute world coordinates (the site group renders at the
|
|
21
|
+
* origin and `terrain-geometry` writes `origin + col * spacing` straight into the
|
|
22
|
+
* buffer), so the datum is a constant rather than a site property. It is named
|
|
23
|
+
* because the *comparison* against it is load-bearing, not the value: it is what
|
|
24
|
+
* separates "the ground" from every other horizontal surface in the scene.
|
|
25
|
+
*/
|
|
26
|
+
export const SITE_DATUM_Y = 0;
|
|
27
|
+
/**
|
|
28
|
+
* How close a surface must sit to the datum to count as the ground.
|
|
29
|
+
*
|
|
30
|
+
* Loose enough to absorb float drift through a level's world matrix, tight enough
|
|
31
|
+
* that a ground slab (5 cm) still reads as a built surface rather than as terrain.
|
|
32
|
+
*/
|
|
33
|
+
export const SITE_DATUM_EPSILON = 1e-4;
|
|
34
|
+
/** True when a world-space height is the site datum — i.e. the terrain's plane. */
|
|
35
|
+
export function isSiteDatum(worldY) {
|
|
36
|
+
return Math.abs(worldY - SITE_DATUM_Y) < SITE_DATUM_EPSILON;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `getLevelElevations` memoized on the `nodes` record identity.
|
|
40
|
+
*
|
|
41
|
+
* Without this the stacking resolver is O(N) per node and therefore O(N²) per
|
|
42
|
+
* frame, since `FloorElevationSystem` calls it for every dirty floor-placed node.
|
|
43
|
+
* The scene store is immutable-by-convention, so the record identity is a correct
|
|
44
|
+
* version key — the same reasoning (and the same `WeakMap` shape) as
|
|
45
|
+
* `terrain-source`'s field cache and the spatial grid's `levelWallsCache`.
|
|
46
|
+
*/
|
|
47
|
+
const levelElevationCache = new WeakMap();
|
|
48
|
+
function cachedLevelElevations(nodes) {
|
|
49
|
+
const cached = levelElevationCache.get(nodes);
|
|
50
|
+
if (cached)
|
|
51
|
+
return cached;
|
|
52
|
+
const elevations = getLevelElevations(nodes);
|
|
53
|
+
levelElevationCache.set(nodes, elevations);
|
|
54
|
+
return elevations;
|
|
55
|
+
}
|
|
56
|
+
function siteOf(nodes) {
|
|
57
|
+
for (const node of Object.values(nodes)) {
|
|
58
|
+
if (node?.type === 'site')
|
|
59
|
+
return node;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Level-local XZ → world XZ, through the building's transform.
|
|
65
|
+
*
|
|
66
|
+
* A level group carries only a Y offset (`LevelSystem` writes `position.y` and
|
|
67
|
+
* nothing else), so level-local XZ is building-local XZ; the building carries the
|
|
68
|
+
* site-space placement, and the site renders at the origin. Only the Y rotation
|
|
69
|
+
* matters — a building tipped in X or Z is not a thing the editor can produce.
|
|
70
|
+
*/
|
|
71
|
+
function levelLocalToWorldXZ(building, x, z) {
|
|
72
|
+
if (!building)
|
|
73
|
+
return [x, z];
|
|
74
|
+
const angle = building.rotation?.[1] ?? 0;
|
|
75
|
+
const [px, , pz] = building.position ?? [0, 0, 0];
|
|
76
|
+
if (angle === 0)
|
|
77
|
+
return [x + px, z + pz];
|
|
78
|
+
const cos = Math.cos(angle);
|
|
79
|
+
const sin = Math.sin(angle);
|
|
80
|
+
return [cos * x + sin * z + px, -sin * x + cos * z + pz];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Where a level sits relative to the datum, for the levels terrain can support.
|
|
84
|
+
*
|
|
85
|
+
* **Only the storey the level stack puts at the datum follows terrain.** An upper
|
|
86
|
+
* storey has a real floor, and draping it would sink its contents into the
|
|
87
|
+
* hillside under the building. The test is that storey's own base height against
|
|
88
|
+
* the datum — deliberately asked of `getLevelElevations`, the same function the
|
|
89
|
+
* renderer positions level groups with, so placement can never hold a second
|
|
90
|
+
* opinion about which storey is on the ground.
|
|
91
|
+
*
|
|
92
|
+
* Which storey that is follows from the stack, not from the ordinal: today the
|
|
93
|
+
* lowest ordinal anchors at 0, so a building with an ordinal `-1` level has *that*
|
|
94
|
+
* level at grade and its ground floor a storey up. When genuine below-grade
|
|
95
|
+
* stacking lands, this rule tracks it with no change here — an excavated basement
|
|
96
|
+
* will stop being at the datum, and stop being draped, on its own.
|
|
97
|
+
*/
|
|
98
|
+
function levelDatum(nodes, levelId) {
|
|
99
|
+
const elevation = cachedLevelElevations(nodes).get(levelId);
|
|
100
|
+
if (!elevation)
|
|
101
|
+
return null;
|
|
102
|
+
const building = elevation.buildingId
|
|
103
|
+
? (nodes[elevation.buildingId] ?? null)
|
|
104
|
+
: null;
|
|
105
|
+
const datum = building?.position?.[1] ?? 0;
|
|
106
|
+
const baseWorldY = datum + elevation.baseY;
|
|
107
|
+
if (!isSiteDatum(baseWorldY))
|
|
108
|
+
return null;
|
|
109
|
+
return { building, baseWorldY };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Whether the sculpted ground is what a node on `levelId` stands on — i.e.
|
|
113
|
+
* whether moving the terrain moves that node.
|
|
114
|
+
*
|
|
115
|
+
* Split out of `terrainSupportLift` so the invalidation rule that re-elevates
|
|
116
|
+
* nodes after a sculpt (`markTerrainSupportDependents`) tests grade-ness with
|
|
117
|
+
* the *same* predicate the resolver does. Two copies of this test would drift,
|
|
118
|
+
* and the failure is silent: a level the resolver drapes but the dirty rule
|
|
119
|
+
* skips leaves its contents frozen at the height the ground used to be.
|
|
120
|
+
*
|
|
121
|
+
* Deliberately independent of whether a field exists. A stroke that clears the
|
|
122
|
+
* terrain has to re-elevate exactly the nodes a stroke that creates it does, and
|
|
123
|
+
* asking about the field here would mark none of them on the way back down.
|
|
124
|
+
*/
|
|
125
|
+
export function isLevelAtSiteDatum(nodes, levelId) {
|
|
126
|
+
return levelDatum(nodes, levelId) !== null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The lift, in level-local metres, that the sculpted ground provides to a node on
|
|
130
|
+
* `levelId` at level-local `x`/`z`. Null when terrain does not support this node —
|
|
131
|
+
* no sculpted ground, an unresolvable level, or a storey whose floor is not at
|
|
132
|
+
* grade.
|
|
133
|
+
*
|
|
134
|
+
* Null rather than 0 so callers keep their existing flat-ground path verbatim.
|
|
135
|
+
* That is what makes this change invisible to every scene that never touched
|
|
136
|
+
* terrain, which is nearly all of them.
|
|
137
|
+
*/
|
|
138
|
+
export function terrainSupportLift(nodes, levelId, x, z) {
|
|
139
|
+
const site = siteOf(nodes);
|
|
140
|
+
const field = site ? terrainFieldOf(site) : null;
|
|
141
|
+
if (!field)
|
|
142
|
+
return null;
|
|
143
|
+
const datum = levelDatum(nodes, levelId);
|
|
144
|
+
if (!datum)
|
|
145
|
+
return null;
|
|
146
|
+
const [worldX, worldZ] = levelLocalToWorldXZ(datum.building, x, z);
|
|
147
|
+
// The lift is measured from the storey's own base, so a building sited on a
|
|
148
|
+
// datum other than 0 does not double-count it.
|
|
149
|
+
return heightAt(field, worldX, worldZ) - datum.baseWorldY;
|
|
150
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { WallNode } from '../schema/nodes/wall';
|
|
2
|
+
import type { AnyNode, AnyNodeId } from '../schema/types';
|
|
3
|
+
/**
|
|
4
|
+
* Pure plan-space wall-distance math shared by the 2D opening snap
|
|
5
|
+
* (`findClosestWallInPlan` in @pascal-app/nodes) and the editor's 2D
|
|
6
|
+
* Voronoi debug overlay. One source of truth means the overlay is a
|
|
7
|
+
* faithful picture of what the snap actually decides.
|
|
8
|
+
*
|
|
9
|
+
* A wall segment's Voronoi cell is exactly "the points whose nearest wall
|
|
10
|
+
* is this segment", so nearest-segment classification == the segment
|
|
11
|
+
* Voronoi diagram. Curved walls are excluded (the opening snap rejects
|
|
12
|
+
* them — mitering + arc + opening tears in 3D).
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Max cursor-to-wall plan distance (metres) for a 2D opening to snap onto a
|
|
16
|
+
* wall. Tight, because plan walls are thin and often close together — a large
|
|
17
|
+
* radius would let a far wall's region reach across a nearer one. Shared so the
|
|
18
|
+
* snap and the Voronoi debug overlay clip to the exact same range.
|
|
19
|
+
*/
|
|
20
|
+
export declare const WALL_SNAP_DISTANCE_M = 0.4;
|
|
21
|
+
export type WallSegment = {
|
|
22
|
+
wall: WallNode;
|
|
23
|
+
/** [x, z] plan start. */
|
|
24
|
+
start: readonly [number, number];
|
|
25
|
+
/** [x, z] plan end. */
|
|
26
|
+
end: readonly [number, number];
|
|
27
|
+
/** Unit direction (start → end) in plan. */
|
|
28
|
+
dirX: number;
|
|
29
|
+
dirY: number;
|
|
30
|
+
/** Segment length in metres. */
|
|
31
|
+
length: number;
|
|
32
|
+
};
|
|
33
|
+
export type WallSegmentClosest = {
|
|
34
|
+
segment: WallSegment;
|
|
35
|
+
/** Distance from the query point to the closest point on the segment. */
|
|
36
|
+
distance: number;
|
|
37
|
+
/** Distance along the wall from `start`, clamped to [0, length]. */
|
|
38
|
+
along: number;
|
|
39
|
+
/** Signed perpendicular offset from the wall axis (+ on the front side). */
|
|
40
|
+
perp: number;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Collect the straight (non-curved) wall segments that are direct children
|
|
44
|
+
* of a level — the candidates an opening can snap onto.
|
|
45
|
+
*/
|
|
46
|
+
export declare function collectLevelWallSegments(nodes: Record<AnyNodeId, AnyNode>, levelId: AnyNodeId | null): WallSegment[];
|
|
47
|
+
/** Closest point + signed offset of one query point against one segment. */
|
|
48
|
+
export declare function closestOnSegment(segment: WallSegment, pointX: number, pointY: number): {
|
|
49
|
+
distance: number;
|
|
50
|
+
along: number;
|
|
51
|
+
perp: number;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* The single nearest wall segment to a plan point — its Voronoi cell. Returns
|
|
55
|
+
* null when `segments` is empty or (when `maxDistance` is given) nothing is
|
|
56
|
+
* within range. Ties resolve to the first segment scanned; callers pass an
|
|
57
|
+
* already-curved-filtered list from `collectLevelWallSegments`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function nearestWallSegment(segments: readonly WallSegment[], pointX: number, pointY: number, maxDistance?: number, excludeWallId?: AnyNodeId): WallSegmentClosest | null;
|
|
60
|
+
//# sourceMappingURL=wall-distance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wall-distance.d.ts","sourceRoot":"","sources":["../../src/lib/wall-distance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAGzD;;;;;;;;;;GAUG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,MAAM,CAAA;AAEvC,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,QAAQ,CAAA;IACd,yBAAyB;IACzB,KAAK,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,uBAAuB;IACvB,GAAG,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,WAAW,CAAA;IACpB,yEAAyE;IACzE,QAAQ,EAAE,MAAM,CAAA;IAChB,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAA;IACb,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EACjC,OAAO,EAAE,SAAS,GAAG,IAAI,GACxB,WAAW,EAAE,CA0Bf;AAED,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CASnD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,SAAS,WAAW,EAAE,EAChC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,WAAW,SAA2B,EACtC,aAAa,CAAC,EAAE,SAAS,GACxB,kBAAkB,GAAG,IAAI,CAU3B"}
|