@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
|
@@ -1,27 +1,22 @@
|
|
|
1
|
+
import { getRenderableSlabPolygon } from '../../lib/slab-polygon';
|
|
2
|
+
import { terrainSupportLift } from '../../lib/terrain-support';
|
|
3
|
+
import { nodeRegistry } from '../../registry';
|
|
1
4
|
import { getScaledDimensions, isLowProfileItemSurface } from '../../schema';
|
|
5
|
+
import { getWallPlaneTop } from '../../services/storey';
|
|
6
|
+
import useLiveNodeOverrides, { getEffectiveNode } from '../../store/use-live-node-overrides';
|
|
7
|
+
import useLiveTransforms from '../../store/use-live-transforms';
|
|
2
8
|
import useScene from '../../store/use-scene';
|
|
3
|
-
import {
|
|
9
|
+
import { computeWallSlabSupport, pointInPolygon, SUPPORT_ELEVATION_EPSILON, } from '../../systems/slab/slab-support';
|
|
4
10
|
import { DEFAULT_WALL_THICKNESS } from '../../systems/wall/wall-footprint';
|
|
11
|
+
import { resolveWallEffectiveHeight } from '../../systems/wall/wall-top';
|
|
12
|
+
import { getFloorPlacedFootprints } from './floor-placed-elevation';
|
|
5
13
|
import { SpatialGrid } from './spatial-grid';
|
|
14
|
+
import { GROUND_SUPPORT_ID } from './support-host-id';
|
|
6
15
|
import { WallSpatialGrid } from './wall-spatial-grid';
|
|
16
|
+
export { computeWallSlabElevation, computeWallSlabSupport, pointInPolygon, SUPPORT_ELEVATION_EPSILON, wallOverlapsPolygon, } from '../../systems/slab/slab-support';
|
|
7
17
|
// ============================================================================
|
|
8
18
|
// GEOMETRY HELPERS
|
|
9
19
|
// ============================================================================
|
|
10
|
-
/**
|
|
11
|
-
* Point-in-polygon test using ray casting algorithm.
|
|
12
|
-
*/
|
|
13
|
-
export function pointInPolygon(px, pz, polygon) {
|
|
14
|
-
let inside = false;
|
|
15
|
-
const n = polygon.length;
|
|
16
|
-
for (let i = 0, j = n - 1; i < n; j = i++) {
|
|
17
|
-
const xi = polygon[i][0], zi = polygon[i][1];
|
|
18
|
-
const xj = polygon[j][0], zj = polygon[j][1];
|
|
19
|
-
if (zi > pz !== zj > pz && px < ((xj - xi) * (pz - zi)) / (zj - zi) + xi) {
|
|
20
|
-
inside = !inside;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return inside;
|
|
24
|
-
}
|
|
25
20
|
/**
|
|
26
21
|
* Compute the 4 XZ footprint corners of an item given its position, dimensions, and Y rotation.
|
|
27
22
|
*/
|
|
@@ -40,6 +35,24 @@ function getItemFootprint(position, dimensions, rotation, inset = 0) {
|
|
|
40
35
|
[x + (-halfW * cos - halfD * sin), z + (-halfW * sin + halfD * cos)],
|
|
41
36
|
];
|
|
42
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Axis-aligned XZ extent of a footprint at `position`, rotated by `yRot`. The
|
|
40
|
+
* rotated width/depth is the same conservative bound the floor-placement draft
|
|
41
|
+
* uses, so a draft and an existing node are compared with identical math.
|
|
42
|
+
*/
|
|
43
|
+
function footprintBoundsXZ(position, dimensions, yRot) {
|
|
44
|
+
const [width, , depth] = dimensions;
|
|
45
|
+
const cos = Math.abs(Math.cos(yRot));
|
|
46
|
+
const sin = Math.abs(Math.sin(yRot));
|
|
47
|
+
const rotatedW = width * cos + depth * sin;
|
|
48
|
+
const rotatedD = width * sin + depth * cos;
|
|
49
|
+
return {
|
|
50
|
+
minX: position[0] - rotatedW / 2,
|
|
51
|
+
maxX: position[0] + rotatedW / 2,
|
|
52
|
+
minZ: position[2] - rotatedD / 2,
|
|
53
|
+
maxZ: position[2] + rotatedD / 2,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
43
56
|
function getItemLocalBounds(item) {
|
|
44
57
|
const [width, height, depth] = getScaledDimensions(item);
|
|
45
58
|
const minZ = item.asset.attachTo === 'wall-side' ? -depth : -depth / 2;
|
|
@@ -99,6 +112,24 @@ function resolveNodeLevelId(node, nodes) {
|
|
|
99
112
|
}
|
|
100
113
|
return 'default';
|
|
101
114
|
}
|
|
115
|
+
function expandIgnoredNodeIds(ignoreIds, nodes) {
|
|
116
|
+
const ignored = new Set(ignoreIds ?? []);
|
|
117
|
+
const queue = [...ignored];
|
|
118
|
+
while (queue.length > 0) {
|
|
119
|
+
const id = queue.pop();
|
|
120
|
+
const node = nodes[id];
|
|
121
|
+
const children = node?.children;
|
|
122
|
+
if (!Array.isArray(children))
|
|
123
|
+
continue;
|
|
124
|
+
for (const childId of children) {
|
|
125
|
+
if (typeof childId !== 'string' || ignored.has(childId))
|
|
126
|
+
continue;
|
|
127
|
+
ignored.add(childId);
|
|
128
|
+
queue.push(childId);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return ignored;
|
|
132
|
+
}
|
|
102
133
|
/**
|
|
103
134
|
* Test if two line segments (a1->a2) and (b1->b2) intersect.
|
|
104
135
|
*/
|
|
@@ -164,169 +195,6 @@ export function itemOverlapsPolygon(position, dimensions, rotation, polygon, ins
|
|
|
164
195
|
}
|
|
165
196
|
return false;
|
|
166
197
|
}
|
|
167
|
-
/**
|
|
168
|
-
* Check if wall segment (a) is substantially on polygon edge segment (b).
|
|
169
|
-
* Returns true only if BOTH endpoints of the wall are on or very close to the edge.
|
|
170
|
-
* This prevents walls that just touch one point from being detected.
|
|
171
|
-
*/
|
|
172
|
-
function segmentsCollinearAndOverlap(ax1, az1, ax2, az2, bx1, bz1, bx2, bz2) {
|
|
173
|
-
const EPSILON = 1e-6;
|
|
174
|
-
// Cross product to check collinearity
|
|
175
|
-
const cross1 = (ax2 - ax1) * (bz1 - az1) - (az2 - az1) * (bx1 - ax1);
|
|
176
|
-
const cross2 = (ax2 - ax1) * (bz2 - az1) - (az2 - az1) * (bx2 - ax1);
|
|
177
|
-
if (Math.abs(cross1) > EPSILON || Math.abs(cross2) > EPSILON) {
|
|
178
|
-
return false; // Not collinear
|
|
179
|
-
}
|
|
180
|
-
// Check if a point is on segment b
|
|
181
|
-
const onSegment = (px, pz, qx, qz, rx, rz) => Math.min(px, qx) - EPSILON <= rx &&
|
|
182
|
-
rx <= Math.max(px, qx) + EPSILON &&
|
|
183
|
-
Math.min(pz, qz) - EPSILON <= rz &&
|
|
184
|
-
rz <= Math.max(pz, qz) + EPSILON;
|
|
185
|
-
// BOTH endpoints of wall (a) must be on edge (b) for substantial overlap
|
|
186
|
-
const a1OnB = onSegment(bx1, bz1, bx2, bz2, ax1, az1);
|
|
187
|
-
const a2OnB = onSegment(bx1, bz1, bx2, bz2, ax2, az2);
|
|
188
|
-
return a1OnB && a2OnB;
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Test if a wall segment overlaps with a polygon.
|
|
192
|
-
* A wall is considered to overlap if:
|
|
193
|
-
* - Its midpoint is inside the polygon (wall crosses through)
|
|
194
|
-
* - At least one endpoint is inside (wall partially or fully in slab)
|
|
195
|
-
* - It's collinear with and overlaps a polygon edge (wall on slab boundary)
|
|
196
|
-
* - (curved walls) any sample along the centerline is inside
|
|
197
|
-
*
|
|
198
|
-
* Note: A wall with just one endpoint touching the edge but the rest outside
|
|
199
|
-
* is NOT considered overlapping (adjacent only).
|
|
200
|
-
*/
|
|
201
|
-
export function wallOverlapsPolygon(startOrWall, endOrPolygon, polygonArg) {
|
|
202
|
-
// Two call shapes:
|
|
203
|
-
// wallOverlapsPolygon(wallLike, polygon) — preferred; curve-aware
|
|
204
|
-
// wallOverlapsPolygon(start, end, polygon) — legacy chord-only
|
|
205
|
-
let start;
|
|
206
|
-
let end;
|
|
207
|
-
let polygon;
|
|
208
|
-
let curveOffset = 0;
|
|
209
|
-
let thickness = DEFAULT_WALL_THICKNESS;
|
|
210
|
-
if (Array.isArray(startOrWall)) {
|
|
211
|
-
start = startOrWall;
|
|
212
|
-
end = endOrPolygon;
|
|
213
|
-
polygon = polygonArg;
|
|
214
|
-
}
|
|
215
|
-
else {
|
|
216
|
-
start = startOrWall.start;
|
|
217
|
-
end = startOrWall.end;
|
|
218
|
-
curveOffset = startOrWall.curveOffset ?? 0;
|
|
219
|
-
thickness = startOrWall.thickness ?? DEFAULT_WALL_THICKNESS;
|
|
220
|
-
polygon = endOrPolygon;
|
|
221
|
-
}
|
|
222
|
-
const halfThickness = Math.max(thickness / 2, 0);
|
|
223
|
-
// Curved walls: sample the centerline. The chord-based checks below miss
|
|
224
|
-
// walls that bow into/out of the slab — e.g. endpoints on the slab
|
|
225
|
-
// boundary with the curve arcing inward through the slab interior. Without
|
|
226
|
-
// this, `getSlabElevationForWall` returns 0 (wall drops to floor) and
|
|
227
|
-
// `markNodesOverlappingSlab` never re-dirties the wall when the slab Y
|
|
228
|
-
// moves.
|
|
229
|
-
if (curveOffset !== 0) {
|
|
230
|
-
const wallLike = { start, end, curveOffset };
|
|
231
|
-
if (isCurvedWall(wallLike)) {
|
|
232
|
-
const samples = sampleWallCenterline(wallLike, 16);
|
|
233
|
-
for (let i = 0; i < samples.length; i++) {
|
|
234
|
-
const point = samples[i];
|
|
235
|
-
if (pointInPolygon(point.x, point.y, polygon))
|
|
236
|
-
return true;
|
|
237
|
-
// Also test ±halfThickness perpendicular at each sample so a curve
|
|
238
|
-
// skirting the slab edge with its centerline just outside still
|
|
239
|
-
// registers — its body sits inside the slab.
|
|
240
|
-
if (halfThickness > 0 && i > 0) {
|
|
241
|
-
const prev = samples[i - 1];
|
|
242
|
-
const sx = point.x - prev.x;
|
|
243
|
-
const sz = point.y - prev.y;
|
|
244
|
-
const sl = Math.sqrt(sx * sx + sz * sz);
|
|
245
|
-
if (sl > 1e-10) {
|
|
246
|
-
const tnx = (-sz / sl) * halfThickness;
|
|
247
|
-
const tnz = (sx / sl) * halfThickness;
|
|
248
|
-
if (pointInPolygon(point.x + tnx, point.y + tnz, polygon))
|
|
249
|
-
return true;
|
|
250
|
-
if (pointInPolygon(point.x - tnx, point.y - tnz, polygon))
|
|
251
|
-
return true;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
const dx = end[0] - start[0];
|
|
258
|
-
const dz = end[1] - start[1];
|
|
259
|
-
const len = Math.sqrt(dx * dx + dz * dz);
|
|
260
|
-
// Nudge endpoint test points a tiny step inward along the wall direction before
|
|
261
|
-
// testing containment. pointInPolygon (ray casting) produces false positives for
|
|
262
|
-
// points exactly on polygon vertices or edges — specifically the minimum-z corner
|
|
263
|
-
// of an axis-aligned polygon returns "inside" because the ray hits the opposite
|
|
264
|
-
// vertical edge exactly at its base. Nudging by 1e-6 m avoids this: a wall that
|
|
265
|
-
// merely starts at a slab corner and extends outward will have its nudged point
|
|
266
|
-
// clearly outside, while a wall that genuinely starts inside stays inside.
|
|
267
|
-
if (len > 1e-10) {
|
|
268
|
-
const step = Math.min(1e-6, len * 0.01);
|
|
269
|
-
const nx = (dx / len) * step;
|
|
270
|
-
const nz = (dz / len) * step;
|
|
271
|
-
if (pointInPolygon(start[0] + nx, start[1] + nz, polygon))
|
|
272
|
-
return true;
|
|
273
|
-
if (pointInPolygon(end[0] - nx, end[1] - nz, polygon))
|
|
274
|
-
return true;
|
|
275
|
-
// Also nudge perpendicular to the wall (into the slab interior) for walls that
|
|
276
|
-
// lie exactly on the slab boundary. The along-wall nudge keeps points on the
|
|
277
|
-
// boundary where pointInPolygon is unreliable; a perpendicular inward nudge
|
|
278
|
-
// moves the point clearly inside (or outside) the polygon.
|
|
279
|
-
// Sample the wall at 1/4, 1/2, 3/4 positions with a perpendicular nudge.
|
|
280
|
-
const PERP_STEP = 1e-4;
|
|
281
|
-
const pnx = (-nz / step) * PERP_STEP; // perpendicular left
|
|
282
|
-
const pnz = (nx / step) * PERP_STEP;
|
|
283
|
-
for (const t of [0.25, 0.5, 0.75]) {
|
|
284
|
-
const bx = start[0] + dx * t;
|
|
285
|
-
const bz = start[1] + dz * t;
|
|
286
|
-
if (pointInPolygon(bx + pnx, bz + pnz, polygon))
|
|
287
|
-
return true;
|
|
288
|
-
if (pointInPolygon(bx - pnx, bz - pnz, polygon))
|
|
289
|
-
return true;
|
|
290
|
-
}
|
|
291
|
-
// Wall-thickness perpendicular test. Walls aren't infinitely thin lines;
|
|
292
|
-
// a wall whose centerline sits just outside the slab boundary still has
|
|
293
|
-
// half its body inside the slab and should follow the slab elevation.
|
|
294
|
-
// Without this, the perimeter walls of a room often miss the slab-overlap
|
|
295
|
-
// detection (the slab polygon is the room's interior, the wall centerline
|
|
296
|
-
// sits on or just outside its edge) and stay at Y=0 while the slab moves
|
|
297
|
-
// up.
|
|
298
|
-
if (halfThickness > 0) {
|
|
299
|
-
const ux = dx / len;
|
|
300
|
-
const uz = dz / len;
|
|
301
|
-
const tnx = -uz * halfThickness;
|
|
302
|
-
const tnz = ux * halfThickness;
|
|
303
|
-
for (const t of [0, 0.25, 0.5, 0.75, 1]) {
|
|
304
|
-
const bx = start[0] + dx * t;
|
|
305
|
-
const bz = start[1] + dz * t;
|
|
306
|
-
if (pointInPolygon(bx + tnx, bz + tnz, polygon))
|
|
307
|
-
return true;
|
|
308
|
-
if (pointInPolygon(bx - tnx, bz - tnz, polygon))
|
|
309
|
-
return true;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
// Check if midpoint is inside (catches walls crossing through)
|
|
314
|
-
const midX = (start[0] + end[0]) / 2;
|
|
315
|
-
const midZ = (start[1] + end[1]) / 2;
|
|
316
|
-
if (pointInPolygon(midX, midZ, polygon))
|
|
317
|
-
return true;
|
|
318
|
-
// Check if the wall is collinear with and overlaps any polygon edge
|
|
319
|
-
const n = polygon.length;
|
|
320
|
-
for (let i = 0; i < n; i++) {
|
|
321
|
-
const j = (i + 1) % n;
|
|
322
|
-
const [p1x, p1z] = polygon[i];
|
|
323
|
-
const [p2x, p2z] = polygon[j];
|
|
324
|
-
if (segmentsCollinearAndOverlap(start[0], start[1], end[0], end[1], p1x, p1z, p2x, p2z)) {
|
|
325
|
-
return true;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
return false;
|
|
329
|
-
}
|
|
330
198
|
export class SpatialGridManager {
|
|
331
199
|
floorGrids = new Map(); // levelId -> grid
|
|
332
200
|
wallGrids = new Map(); // levelId -> wall grid
|
|
@@ -361,7 +229,14 @@ export class SpatialGridManager {
|
|
|
361
229
|
}
|
|
362
230
|
getWallHeight(wallId) {
|
|
363
231
|
const wall = this.walls.get(wallId);
|
|
364
|
-
|
|
232
|
+
if (!wall)
|
|
233
|
+
return 0;
|
|
234
|
+
if (wall.height != null)
|
|
235
|
+
return wall.height;
|
|
236
|
+
const nodes = useScene.getState().nodes;
|
|
237
|
+
const levelId = resolveNodeLevelId(wall, nodes);
|
|
238
|
+
const support = this.getSlabSupportForWall(levelId, wall.start, wall.end, wall.curveOffset ?? 0, wall.thickness, wall.supportSlabId ?? null, undefined, wall.supportOffset);
|
|
239
|
+
return resolveWallEffectiveHeight(wall, getWallPlaneTop(wall, levelId, nodes), support.elevation);
|
|
365
240
|
}
|
|
366
241
|
getCeilingGrid(ceilingId) {
|
|
367
242
|
if (!this.ceilingGrids.has(ceilingId)) {
|
|
@@ -375,10 +250,122 @@ export class SpatialGridManager {
|
|
|
375
250
|
}
|
|
376
251
|
return this.slabsByLevel.get(levelId);
|
|
377
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Per-slab RENDERED polygon cache (`getRenderableSlabPolygon`). Item
|
|
255
|
+
* support queries run per frame and the projection scans the level's
|
|
256
|
+
* walls + sibling slabs, so the result is cached per slab id and
|
|
257
|
+
* dropped for the whole level whenever a slab or wall on that level
|
|
258
|
+
* flows through the manager's create/update/delete handlers.
|
|
259
|
+
*/
|
|
260
|
+
renderedSlabPolygons = new Map();
|
|
261
|
+
invalidateRenderedSlabPolygons(levelId) {
|
|
262
|
+
const slabMap = this.slabsByLevel.get(levelId);
|
|
263
|
+
if (!slabMap)
|
|
264
|
+
return;
|
|
265
|
+
for (const slabId of slabMap.keys())
|
|
266
|
+
this.renderedSlabPolygons.delete(slabId);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* True while a slab or wall on `levelId` has a live preview: group drags
|
|
270
|
+
* publish translated slab polygons and wall endpoints to
|
|
271
|
+
* `useLiveNodeOverrides`, and the slab move tool / room-preset stamp
|
|
272
|
+
* publish a translation DELTA to `useLiveTransforms` — either way the
|
|
273
|
+
* scene store commits only on release, so the committed cache and index
|
|
274
|
+
* would elect support against pre-drag footprints (items and walls
|
|
275
|
+
* visibly drop to ground mid-preview). Support queries then read
|
|
276
|
+
* live-effective records and skip the rendered-polygon cache.
|
|
277
|
+
*/
|
|
278
|
+
levelHasLivePreview(levelId) {
|
|
279
|
+
const nodes = useScene.getState().nodes;
|
|
280
|
+
const structuralOnLevel = (id) => {
|
|
281
|
+
const node = nodes[id];
|
|
282
|
+
if (!node || (node.type !== 'slab' && node.type !== 'wall'))
|
|
283
|
+
return false;
|
|
284
|
+
return resolveNodeLevelId(node, nodes) === levelId;
|
|
285
|
+
};
|
|
286
|
+
const overrides = useLiveNodeOverrides.getState().overrides;
|
|
287
|
+
for (const id of overrides.keys()) {
|
|
288
|
+
if (structuralOnLevel(id))
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
const transforms = useLiveTransforms.getState().transforms;
|
|
292
|
+
for (const id of transforms.keys()) {
|
|
293
|
+
if (structuralOnLevel(id))
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* The live-effective slab record: field overrides merged, then the
|
|
300
|
+
* `useLiveTransforms` DELTA (slab publishers — move tool, room-preset
|
|
301
|
+
* stamp — store a translation, not an absolute position) applied to the
|
|
302
|
+
* polygon, holes, and elevation. Mapping happens exactly ONCE at each
|
|
303
|
+
* public query's loop entry: `slabSupportsFootprint` /
|
|
304
|
+
* `getRenderedSlabPolygon` take the already-effective record and must
|
|
305
|
+
* never re-map, or the delta would apply twice.
|
|
306
|
+
*/
|
|
307
|
+
effectiveSlabRecord(slab) {
|
|
308
|
+
let effective = getEffectiveNode(slab);
|
|
309
|
+
const live = useLiveTransforms.getState().get(slab.id);
|
|
310
|
+
if (live) {
|
|
311
|
+
const [dx, dy, dz] = live.position;
|
|
312
|
+
if (dx !== 0 || dy !== 0 || dz !== 0) {
|
|
313
|
+
effective = {
|
|
314
|
+
...effective,
|
|
315
|
+
polygon: effective.polygon.map(([x, z]) => [x + dx, z + dz]),
|
|
316
|
+
holes: (effective.holes || []).map((hole) => hole.map(([x, z]) => [x + dx, z + dz])),
|
|
317
|
+
elevation: (effective.elevation ?? 0.05) + dy,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return effective;
|
|
322
|
+
}
|
|
323
|
+
getRenderedSlabPolygon(levelId, slab) {
|
|
324
|
+
const live = this.levelHasLivePreview(levelId);
|
|
325
|
+
if (!live) {
|
|
326
|
+
const cached = this.renderedSlabPolygons.get(slab.id);
|
|
327
|
+
if (cached)
|
|
328
|
+
return cached;
|
|
329
|
+
}
|
|
330
|
+
const siblingSlabs = [];
|
|
331
|
+
for (const other of this.getSlabMap(levelId).values()) {
|
|
332
|
+
if (other.id !== slab.id)
|
|
333
|
+
siblingSlabs.push(live ? this.effectiveSlabRecord(other) : other);
|
|
334
|
+
}
|
|
335
|
+
const walls = this.getLevelWallNodes(levelId);
|
|
336
|
+
const polygon = getRenderableSlabPolygon(slab, {
|
|
337
|
+
walls: live ? walls.map((wall) => getEffectiveNode(wall)) : walls,
|
|
338
|
+
siblingSlabs,
|
|
339
|
+
});
|
|
340
|
+
if (!live)
|
|
341
|
+
this.renderedSlabPolygons.set(slab.id, polygon);
|
|
342
|
+
return polygon;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Support test shared by election, candidate listing, and persisted-host
|
|
346
|
+
* validation: the footprint overlaps the slab's RENDERED polygon (what
|
|
347
|
+
* users see — matching the wall election in `computeWallSlabSupport`),
|
|
348
|
+
* with the center-point hole veto kept against the stored holes (holes
|
|
349
|
+
* are data, never render-offset).
|
|
350
|
+
*/
|
|
351
|
+
slabSupportsFootprint(levelId, slab, position, dimensions, rotation) {
|
|
352
|
+
if (slab.polygon.length < 3)
|
|
353
|
+
return false;
|
|
354
|
+
const rendered = this.getRenderedSlabPolygon(levelId, slab);
|
|
355
|
+
if (!itemOverlapsPolygon(position, dimensions, rotation, rendered, 0.01))
|
|
356
|
+
return false;
|
|
357
|
+
const [cx, , cz] = position;
|
|
358
|
+
for (const hole of slab.holes || []) {
|
|
359
|
+
if (hole.length >= 3 && pointInPolygon(cx, cz, hole))
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
378
364
|
// Called when nodes change
|
|
379
365
|
handleNodeCreated(node, levelId) {
|
|
380
366
|
if (node.type === 'slab') {
|
|
381
367
|
this.getSlabMap(levelId).set(node.id, node);
|
|
368
|
+
this.invalidateRenderedSlabPolygons(levelId);
|
|
382
369
|
}
|
|
383
370
|
else if (node.type === 'ceiling') {
|
|
384
371
|
this.ceilings.set(node.id, node);
|
|
@@ -386,6 +373,8 @@ export class SpatialGridManager {
|
|
|
386
373
|
else if (node.type === 'wall') {
|
|
387
374
|
const wall = node;
|
|
388
375
|
this.walls.set(wall.id, wall);
|
|
376
|
+
// Rendered slab polygons adopt wall bands — a new wall can extend them.
|
|
377
|
+
this.invalidateRenderedSlabPolygons(levelId);
|
|
389
378
|
}
|
|
390
379
|
else if (node.type === 'item') {
|
|
391
380
|
const item = node;
|
|
@@ -430,6 +419,7 @@ export class SpatialGridManager {
|
|
|
430
419
|
handleNodeUpdated(node, levelId) {
|
|
431
420
|
if (node.type === 'slab') {
|
|
432
421
|
this.getSlabMap(levelId).set(node.id, node);
|
|
422
|
+
this.invalidateRenderedSlabPolygons(levelId);
|
|
433
423
|
}
|
|
434
424
|
else if (node.type === 'ceiling') {
|
|
435
425
|
this.ceilings.set(node.id, node);
|
|
@@ -437,6 +427,7 @@ export class SpatialGridManager {
|
|
|
437
427
|
else if (node.type === 'wall') {
|
|
438
428
|
const wall = node;
|
|
439
429
|
this.walls.set(wall.id, wall);
|
|
430
|
+
this.invalidateRenderedSlabPolygons(levelId);
|
|
440
431
|
}
|
|
441
432
|
else if (node.type === 'item') {
|
|
442
433
|
const item = node;
|
|
@@ -486,6 +477,9 @@ export class SpatialGridManager {
|
|
|
486
477
|
}
|
|
487
478
|
handleNodeDeleted(nodeId, nodeType, levelId) {
|
|
488
479
|
if (nodeType === 'slab') {
|
|
480
|
+
// Invalidate before removal so the deleted slab's own cache entry
|
|
481
|
+
// (still keyed in the level map here) is dropped with its siblings'.
|
|
482
|
+
this.invalidateRenderedSlabPolygons(levelId);
|
|
489
483
|
this.getSlabMap(levelId).delete(nodeId);
|
|
490
484
|
}
|
|
491
485
|
else if (nodeType === 'ceiling') {
|
|
@@ -494,6 +488,7 @@ export class SpatialGridManager {
|
|
|
494
488
|
}
|
|
495
489
|
else if (nodeType === 'wall') {
|
|
496
490
|
this.walls.delete(nodeId);
|
|
491
|
+
this.invalidateRenderedSlabPolygons(levelId);
|
|
497
492
|
// Remove all items attached to this wall from the spatial grid
|
|
498
493
|
const removedItemIds = this.getWallGrid(levelId).removeWall(nodeId);
|
|
499
494
|
return removedItemIds; // Caller can use this to delete the items from scene
|
|
@@ -512,37 +507,51 @@ export class SpatialGridManager {
|
|
|
512
507
|
}
|
|
513
508
|
// Query methods
|
|
514
509
|
canPlaceOnFloor(levelId, position, dimensions, rotation, ignoreIds) {
|
|
510
|
+
return this.canPlaceOnFloorFootprints(levelId, [{ position, dimensions, rotation }], ignoreIds);
|
|
511
|
+
}
|
|
512
|
+
canPlaceOnFloorFootprints(levelId, footprints, ignoreIds) {
|
|
515
513
|
const nodes = useScene.getState().nodes;
|
|
516
|
-
const ignoreSet =
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
514
|
+
const ignoreSet = expandIgnoredNodeIds(ignoreIds, nodes);
|
|
515
|
+
const draftBounds = footprints.map((footprint) => footprintBoundsXZ(footprint.position, footprint.dimensions, footprint.rotation[1] ?? 0));
|
|
516
|
+
for (let i = 0; i < draftBounds.length; i += 1) {
|
|
517
|
+
const a = draftBounds[i];
|
|
518
|
+
for (let j = i + 1; j < draftBounds.length; j += 1) {
|
|
519
|
+
const b = draftBounds[j];
|
|
520
|
+
if (intervalsOverlap(a.minX, a.maxX, b.minX, b.maxX) &&
|
|
521
|
+
intervalsOverlap(a.minZ, a.maxZ, b.minZ, b.maxZ)) {
|
|
522
|
+
return { valid: false, conflictIds: [] };
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// A floor placement conflicts with any other COLLIDING floor-resting node,
|
|
527
|
+
// not just items — every kind whose `floorPlaced.collides` is set (item /
|
|
528
|
+
// shelf / column / cabinet / stair) contributes its footprint(s) as an
|
|
529
|
+
// obstacle. Each candidate's XZ extent is read from the same declarative
|
|
530
|
+
// footprint the elevation + sync paths use, so adding a colliding kind
|
|
531
|
+
// needs no change here.
|
|
529
532
|
const conflicts = [];
|
|
530
533
|
for (const node of Object.values(nodes)) {
|
|
531
|
-
if (node.
|
|
534
|
+
if (ignoreSet.has(node.id))
|
|
532
535
|
continue;
|
|
533
|
-
const
|
|
534
|
-
if (
|
|
536
|
+
const floorPlaced = nodeRegistry.get(node.type)?.capabilities?.floorPlaced;
|
|
537
|
+
if (!floorPlaced?.collides)
|
|
535
538
|
continue;
|
|
536
|
-
if (
|
|
539
|
+
if (floorPlaced.applies && !floorPlaced.applies(node))
|
|
537
540
|
continue;
|
|
538
|
-
|
|
541
|
+
// Low-profile item surfaces (rugs, mats) are stack-on targets, not
|
|
542
|
+
// obstacles — keep the long-standing item-only exemption.
|
|
543
|
+
if (node.type === 'item' && isLowProfileItemSurface(node))
|
|
539
544
|
continue;
|
|
540
|
-
if (resolveNodeLevelId(
|
|
545
|
+
if (resolveNodeLevelId(node, nodes) !== levelId)
|
|
541
546
|
continue;
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
547
|
+
for (const footprint of getFloorPlacedFootprints(floorPlaced, node, { nodes })) {
|
|
548
|
+
const fpRotation = Array.isArray(footprint.rotation) ? (footprint.rotation[1] ?? 0) : 0;
|
|
549
|
+
const bounds = footprintBoundsXZ(footprint.position ?? node.position, footprint.dimensions, fpRotation);
|
|
550
|
+
if (draftBounds.some((draft) => intervalsOverlap(draft.minX, draft.maxX, bounds.minX, bounds.maxX) &&
|
|
551
|
+
intervalsOverlap(draft.minZ, draft.maxZ, bounds.minZ, bounds.maxZ))) {
|
|
552
|
+
conflicts.push(node.id);
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
546
555
|
}
|
|
547
556
|
}
|
|
548
557
|
return { valid: conflicts.length === 0, conflictIds: conflicts };
|
|
@@ -617,7 +626,8 @@ export class SpatialGridManager {
|
|
|
617
626
|
if (!slabMap)
|
|
618
627
|
return 0;
|
|
619
628
|
let maxElevation = 0;
|
|
620
|
-
for (const
|
|
629
|
+
for (const stored of slabMap.values()) {
|
|
630
|
+
const slab = this.effectiveSlabRecord(stored);
|
|
621
631
|
if (slab.polygon.length >= 3 && pointInPolygon(x, z, slab.polygon)) {
|
|
622
632
|
// Check if point is in any hole
|
|
623
633
|
let inHole = false;
|
|
@@ -640,96 +650,222 @@ export class SpatialGridManager {
|
|
|
640
650
|
}
|
|
641
651
|
/**
|
|
642
652
|
* Get the slab elevation for an item using its full footprint (bounding box).
|
|
643
|
-
*
|
|
644
|
-
*
|
|
653
|
+
* Thin wrapper over {@link getSlabSupportForItem} for callers (and tests)
|
|
654
|
+
* that only need the number.
|
|
655
|
+
*/
|
|
656
|
+
getSlabElevationForItem(levelId, position, dimensions, rotation, maxElevation) {
|
|
657
|
+
return this.getSlabSupportForItem(levelId, position, dimensions, rotation, maxElevation)
|
|
658
|
+
.elevation;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Elect the supporting slab for a footprint: the highest-elevation slab
|
|
662
|
+
* whose RENDERED polygon the footprint overlaps (center-point hole veto
|
|
663
|
+
* applies). Returns `{ elevation: 0, slabId: null }` when nothing
|
|
664
|
+
* overlaps.
|
|
665
|
+
*
|
|
666
|
+
* `maxElevation` is the pointer-decided cap: when set, only slabs whose
|
|
667
|
+
* walking surface sits at or below `maxElevation +
|
|
668
|
+
* SUPPORT_ELEVATION_EPSILON` may win — a deck hanging above the surface
|
|
669
|
+
* the cursor ray actually hit never captures the election.
|
|
645
670
|
*/
|
|
646
|
-
|
|
671
|
+
getSlabSupportForItem(levelId, position, dimensions, rotation, maxElevation) {
|
|
647
672
|
const slabMap = this.slabsByLevel.get(levelId);
|
|
648
673
|
if (!slabMap)
|
|
649
|
-
return 0;
|
|
650
|
-
let
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
674
|
+
return { elevation: 0, slabId: null };
|
|
675
|
+
let winningElevation = Number.NEGATIVE_INFINITY;
|
|
676
|
+
let winnerId = null;
|
|
677
|
+
for (const stored of slabMap.values()) {
|
|
678
|
+
const slab = this.effectiveSlabRecord(stored);
|
|
679
|
+
const elevation = slab.elevation ?? 0.05;
|
|
680
|
+
if (maxElevation != null && elevation > maxElevation + SUPPORT_ELEVATION_EPSILON)
|
|
681
|
+
continue;
|
|
682
|
+
if (!this.slabSupportsFootprint(levelId, slab, position, dimensions, rotation))
|
|
683
|
+
continue;
|
|
684
|
+
if (elevation > winningElevation) {
|
|
685
|
+
winningElevation = elevation;
|
|
686
|
+
winnerId = slab.id;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return winnerId === null
|
|
690
|
+
? { elevation: 0, slabId: null }
|
|
691
|
+
: { elevation: winningElevation, slabId: winnerId };
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* The walking surface the pointer actually points at: the nearest slab
|
|
695
|
+
* plane the ray crosses INSIDE that slab's rendered polygon (hole veto
|
|
696
|
+
* applies), or the level base (`elevation: 0, slabId: null`) when it
|
|
697
|
+
* crosses none. Ray origin/direction are level-local. Deliberately a
|
|
698
|
+
* point test, not a footprint test — it answers "which surface is under
|
|
699
|
+
* the cursor", which then caps the footprint election so a deck hanging
|
|
700
|
+
* above the aimed-at floor never lifts the placement. `point` is the
|
|
701
|
+
* ray's crossing of that surface's plane — the stable plan point
|
|
702
|
+
* callers should elect/preview at (see {@link PointedSupportSurface}).
|
|
703
|
+
*/
|
|
704
|
+
getPointedSupportSurface(levelId, rayOrigin, rayDirection) {
|
|
705
|
+
const slabMap = this.slabsByLevel.get(levelId);
|
|
706
|
+
const [ox, oy, oz] = rayOrigin;
|
|
707
|
+
const [dx, dy, dz] = rayDirection;
|
|
708
|
+
if (Math.abs(dy) < 1e-9)
|
|
709
|
+
return { elevation: 0, slabId: null, point: null };
|
|
710
|
+
let best = null;
|
|
711
|
+
if (slabMap) {
|
|
712
|
+
for (const stored of slabMap.values()) {
|
|
713
|
+
const slab = this.effectiveSlabRecord(stored);
|
|
714
|
+
if (slab.polygon.length < 3)
|
|
715
|
+
continue;
|
|
716
|
+
const elevation = slab.elevation ?? 0.05;
|
|
717
|
+
const t = (elevation - oy) / dy;
|
|
718
|
+
if (t <= 0)
|
|
719
|
+
continue;
|
|
720
|
+
if (best && t >= best.t)
|
|
721
|
+
continue;
|
|
722
|
+
const x = ox + dx * t;
|
|
723
|
+
const z = oz + dz * t;
|
|
724
|
+
const rendered = this.getRenderedSlabPolygon(levelId, slab);
|
|
725
|
+
if (rendered.length < 3 || !pointInPolygon(x, z, rendered))
|
|
726
|
+
continue;
|
|
656
727
|
let inHole = false;
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
for (const hole of holes) {
|
|
660
|
-
if (hole.length >= 3 && pointInPolygon(cx, cz, hole)) {
|
|
728
|
+
for (const hole of slab.holes || []) {
|
|
729
|
+
if (hole.length >= 3 && pointInPolygon(x, z, hole)) {
|
|
661
730
|
inHole = true;
|
|
662
731
|
break;
|
|
663
732
|
}
|
|
664
733
|
}
|
|
665
|
-
if (
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
maxElevation = elevation;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
734
|
+
if (inHole)
|
|
735
|
+
continue;
|
|
736
|
+
best = { t, elevation, slabId: slab.id };
|
|
671
737
|
}
|
|
672
738
|
}
|
|
673
|
-
|
|
739
|
+
if (best) {
|
|
740
|
+
return {
|
|
741
|
+
elevation: best.elevation,
|
|
742
|
+
slabId: best.slabId,
|
|
743
|
+
point: [ox + dx * best.t, oz + dz * best.t],
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
const tBase = -oy / dy;
|
|
747
|
+
return {
|
|
748
|
+
elevation: 0,
|
|
749
|
+
slabId: null,
|
|
750
|
+
point: tBase > 0 ? [ox + dx * tBase, oz + dz * tBase] : null,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* All slabs supporting a footprint, one entry per overlapping slab
|
|
755
|
+
* (highest elevation first; slab id breaks ties deterministically).
|
|
756
|
+
* Commit-side ambiguity check: persist a `supportSlabId` only when the
|
|
757
|
+
* candidates carry ≥ 2 distinct elevations.
|
|
758
|
+
*/
|
|
759
|
+
getSupportCandidatesForFootprint(levelId, position, dimensions, rotation) {
|
|
760
|
+
const slabMap = this.slabsByLevel.get(levelId);
|
|
761
|
+
if (!slabMap)
|
|
762
|
+
return [];
|
|
763
|
+
const candidates = [];
|
|
764
|
+
for (const stored of slabMap.values()) {
|
|
765
|
+
const slab = this.effectiveSlabRecord(stored);
|
|
766
|
+
if (!this.slabSupportsFootprint(levelId, slab, position, dimensions, rotation))
|
|
767
|
+
continue;
|
|
768
|
+
candidates.push({ slabId: slab.id, elevation: slab.elevation ?? 0.05 });
|
|
769
|
+
}
|
|
770
|
+
candidates.sort((a, b) => b.elevation - a.elevation || (a.slabId < b.slabId ? -1 : a.slabId > b.slabId ? 1 : 0));
|
|
771
|
+
return candidates;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Elevation of a persisted support host for a footprint, or null when
|
|
775
|
+
* the slab no longer exists on the level or no longer overlaps the
|
|
776
|
+
* footprint (same overlap test as election). Deliberately read-only: a
|
|
777
|
+
* host reshaped away is NOT cleared — callers fall back to election and
|
|
778
|
+
* the stale reference resumes hosting if the slab's polygon returns.
|
|
779
|
+
* Slab deletion is the only writer (`deleteNodesAction` strips it).
|
|
780
|
+
*/
|
|
781
|
+
getHostSlabElevationForFootprint(levelId, slabId, position, dimensions, rotation) {
|
|
782
|
+
const stored = this.slabsByLevel.get(levelId)?.get(slabId);
|
|
783
|
+
if (!stored)
|
|
784
|
+
return null;
|
|
785
|
+
const slab = this.effectiveSlabRecord(stored);
|
|
786
|
+
if (!this.slabSupportsFootprint(levelId, slab, position, dimensions, rotation))
|
|
787
|
+
return null;
|
|
788
|
+
return slab.elevation ?? 0.05;
|
|
674
789
|
}
|
|
675
790
|
/**
|
|
676
791
|
* Get the slab elevation for a wall by checking if it overlaps with any slab polygon (excluding holes).
|
|
677
|
-
* Uses wallOverlapsPolygon which handles edge cases (points on boundary, collinear segments).
|
|
678
792
|
* Returns the highest slab elevation found, or 0 if none.
|
|
679
793
|
*
|
|
680
794
|
* Accepts an optional `curveOffset` so curved walls evaluate overlap
|
|
681
795
|
* against their actual centerline samples, not just the chord.
|
|
682
796
|
*/
|
|
683
|
-
getSlabElevationForWall(levelId, start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS) {
|
|
797
|
+
getSlabElevationForWall(levelId, start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS, preferredSlabId) {
|
|
798
|
+
return this.getSlabSupportForWall(levelId, start, end, curveOffset, thickness, preferredSlabId)
|
|
799
|
+
.elevation;
|
|
800
|
+
}
|
|
801
|
+
getSlabSupportForWall(levelId, start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS, preferredSlabId, maxElevation, supportOffset = 0) {
|
|
802
|
+
if (preferredSlabId === GROUND_SUPPORT_ID) {
|
|
803
|
+
const nodes = useScene.getState().nodes;
|
|
804
|
+
const elevation = (terrainSupportLift(nodes, levelId, start[0], start[1]) ?? 0) + supportOffset;
|
|
805
|
+
return {
|
|
806
|
+
elevation,
|
|
807
|
+
electedSlabId: null,
|
|
808
|
+
baseElevation: elevation,
|
|
809
|
+
baseSegments: [{ start: 0, end: 1, elevation }],
|
|
810
|
+
};
|
|
811
|
+
}
|
|
684
812
|
const slabMap = this.slabsByLevel.get(levelId);
|
|
685
|
-
if (!slabMap)
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
813
|
+
if (!slabMap) {
|
|
814
|
+
const elevation = supportOffset;
|
|
815
|
+
return {
|
|
816
|
+
elevation,
|
|
817
|
+
electedSlabId: null,
|
|
818
|
+
baseElevation: elevation,
|
|
819
|
+
baseSegments: [{ start: 0, end: 1, elevation }],
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
const support = computeWallSlabSupport({ start, end, curveOffset, thickness }, [...slabMap.values()].map((slab) => this.effectiveSlabRecord(slab)), this.getLevelWallNodes(levelId).map((wall) => getEffectiveNode(wall)), preferredSlabId, maxElevation);
|
|
823
|
+
if (supportOffset === 0)
|
|
824
|
+
return support;
|
|
825
|
+
return {
|
|
826
|
+
...support,
|
|
827
|
+
elevation: support.elevation + supportOffset,
|
|
828
|
+
baseElevation: support.baseElevation + supportOffset,
|
|
829
|
+
baseSegments: support.baseSegments.map((segment) => ({
|
|
830
|
+
...segment,
|
|
831
|
+
elevation: segment.elevation + supportOffset,
|
|
832
|
+
})),
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Walls on a level, resolved fresh from the scene store (the manager's
|
|
837
|
+
* own wall map is only maintained on create/delete, not on updates).
|
|
838
|
+
* Cached per scene `nodes` record so per-pointer-tick callers
|
|
839
|
+
* (door/window move) don't rescan the node map.
|
|
840
|
+
*/
|
|
841
|
+
levelWallsCache = new WeakMap();
|
|
842
|
+
getLevelWallNodes(levelId) {
|
|
843
|
+
const nodes = useScene.getState().nodes;
|
|
844
|
+
let byLevel = this.levelWallsCache.get(nodes);
|
|
845
|
+
if (!byLevel) {
|
|
846
|
+
byLevel = new Map();
|
|
847
|
+
this.levelWallsCache.set(nodes, byLevel);
|
|
848
|
+
}
|
|
849
|
+
const cached = byLevel.get(levelId);
|
|
850
|
+
if (cached)
|
|
851
|
+
return cached;
|
|
852
|
+
const walls = [];
|
|
853
|
+
for (const node of Object.values(nodes)) {
|
|
854
|
+
if (node.type !== 'wall')
|
|
707
855
|
continue;
|
|
856
|
+
// Walk the parent chain to the owning level (guarded against cycles).
|
|
857
|
+
let current = node;
|
|
858
|
+
let guard = 0;
|
|
859
|
+
while (current && current.type !== 'level' && guard < 16) {
|
|
860
|
+
current = current.parentId ? nodes[current.parentId] : undefined;
|
|
861
|
+
guard += 1;
|
|
708
862
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
// midpoint falls in a staircase hole but the wall's endpoints are on solid slab.
|
|
712
|
-
let hasValidPoint = false;
|
|
713
|
-
for (const sample of holeSamplePoints) {
|
|
714
|
-
let inHole = false;
|
|
715
|
-
for (const hole of holes) {
|
|
716
|
-
if (hole.length >= 3 && pointInPolygon(sample.x, sample.y, hole)) {
|
|
717
|
-
inHole = true;
|
|
718
|
-
break;
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
if (!inHole) {
|
|
722
|
-
hasValidPoint = true;
|
|
723
|
-
break;
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
if (hasValidPoint) {
|
|
727
|
-
const elevation = slab.elevation ?? 0.05;
|
|
728
|
-
if (elevation > maxElevation)
|
|
729
|
-
maxElevation = elevation;
|
|
863
|
+
if (current?.type === 'level' && current.id === levelId) {
|
|
864
|
+
walls.push(node);
|
|
730
865
|
}
|
|
731
866
|
}
|
|
732
|
-
|
|
867
|
+
byLevel.set(levelId, walls);
|
|
868
|
+
return walls;
|
|
733
869
|
}
|
|
734
870
|
/**
|
|
735
871
|
* Check if an item can be placed on a ceiling.
|
|
@@ -789,6 +925,7 @@ export class SpatialGridManager {
|
|
|
789
925
|
return { valid: conflicts.length === 0, conflictIds: conflicts };
|
|
790
926
|
}
|
|
791
927
|
clearLevel(levelId) {
|
|
928
|
+
this.invalidateRenderedSlabPolygons(levelId);
|
|
792
929
|
this.floorGrids.delete(levelId);
|
|
793
930
|
this.wallGrids.delete(levelId);
|
|
794
931
|
this.slabsByLevel.delete(levelId);
|
|
@@ -801,7 +938,26 @@ export class SpatialGridManager {
|
|
|
801
938
|
this.ceilingGrids.clear();
|
|
802
939
|
this.ceilings.clear();
|
|
803
940
|
this.itemCeilingMap.clear();
|
|
941
|
+
this.renderedSlabPolygons.clear();
|
|
804
942
|
}
|
|
805
943
|
}
|
|
806
944
|
// Singleton instance
|
|
807
945
|
export const spatialGridManager = new SpatialGridManager();
|
|
946
|
+
/** Level-local Y where the rendered wall mesh begins. */
|
|
947
|
+
export function getWallBaseElevationForNodes(wall, nodes) {
|
|
948
|
+
const levelId = resolveNodeLevelId(wall, nodes);
|
|
949
|
+
return spatialGridManager.getSlabSupportForWall(levelId, wall.start, wall.end, wall.curveOffset ?? 0, wall.thickness, wall.supportSlabId ?? null, undefined, wall.supportOffset).elevation;
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Effective (extruded) height of a wall resolved from a nodes record:
|
|
953
|
+
* {@link resolveWallEffectiveHeight} over the covering-clamped plane top
|
|
954
|
+
* (`getWallPlaneTop`) and the singleton manager's slab election — so the
|
|
955
|
+
* value always agrees with the rendered wall. One shared resolver for the
|
|
956
|
+
* editor overlays (measurement label, action menu, side handles) that used
|
|
957
|
+
* to copy this derivation locally.
|
|
958
|
+
*/
|
|
959
|
+
export function getWallEffectiveHeightForNodes(wall, nodes) {
|
|
960
|
+
const levelId = resolveNodeLevelId(wall, nodes);
|
|
961
|
+
const baseElevation = getWallBaseElevationForNodes(wall, nodes);
|
|
962
|
+
return resolveWallEffectiveHeight(wall, getWallPlaneTop(wall, levelId, nodes), baseElevation);
|
|
963
|
+
}
|