@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,14 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { GROUND_SUPPORT_ID } from '../hooks/spatial-grid/support-host-id';
|
|
2
|
+
import { CeilingNode, SlabNode, ZoneNode, } from '../schema';
|
|
3
|
+
import { DEFAULT_LEVEL_HEIGHT } from '../services/level-height';
|
|
4
|
+
import { CEILING_CLAMP_MARGIN, findLevelAboveId, getCeilingClampBound, getLevelElevations, getStoredLevelHeight, } from '../services/storey';
|
|
2
5
|
import { getSceneHistoryPauseDepth, pauseSceneHistory, resumeSceneHistory, } from '../store/history-control';
|
|
6
|
+
import { computeWallSlabSupport } from '../systems/slab/slab-support';
|
|
3
7
|
import { getClampedWallCurveOffset, getWallCurveFrameAt, isCurvedWall, } from '../systems/wall/wall-curve';
|
|
8
|
+
import { resolveWallTop } from '../systems/wall/wall-top';
|
|
4
9
|
import { simplifyClosedPolygon } from './polygon-geometry';
|
|
10
|
+
import { terrainSupportLift } from './terrain-support';
|
|
5
11
|
const DEFAULT_AUTO_SLAB_ELEVATION = 0.05;
|
|
6
|
-
const DEFAULT_AUTO_CEILING_HEIGHT = 2.5;
|
|
7
12
|
const CEILING_HEIGHT_EPSILON = 1e-6;
|
|
8
13
|
const ROOM_CURVE_TOLERANCE = 0.04;
|
|
9
14
|
const MAX_CURVE_SUBDIVISION_DEPTH = 6;
|
|
10
15
|
const AUTO_SLAB_POLYGON_SIMPLIFY_TOLERANCE = 0.08;
|
|
11
16
|
const WALL_ROOM_BOUNDARY_TOLERANCE = 0.08;
|
|
17
|
+
// A wall endpoint within this distance of another wall's interior is treated as a
|
|
18
|
+
// T-junction and splits that wall (see `splitStraightWallAtVertices`).
|
|
19
|
+
const WALL_JUNCTION_TOLERANCE = 0.08;
|
|
20
|
+
// An unmatched auto slab/ceiling whose polygon is still substantially covered
|
|
21
|
+
// by a detected room was absorbed by a room merge — the surviving auto surface
|
|
22
|
+
// owns that area, so keeping it would z-fight and it is deleted. Below this
|
|
23
|
+
// coverage the room genuinely ceased to exist (e.g. an enclosing wall was
|
|
24
|
+
// deleted) and the node is demoted to manual so user data survives.
|
|
25
|
+
const ORPHAN_MERGE_COVERAGE_THRESHOLD = 0.6;
|
|
26
|
+
const COVERAGE_SAMPLE_STEPS = 12;
|
|
27
|
+
const ROOM_VERTICAL_PLANE_EPSILON = 1e-3;
|
|
12
28
|
function pointFromTuple(point) {
|
|
13
29
|
return { x: point[0], y: point[1] };
|
|
14
30
|
}
|
|
@@ -116,6 +132,46 @@ function bboxOverlapArea(a, b) {
|
|
|
116
132
|
const iy = Math.max(0, Math.min(a.maxY, b.maxY) - Math.max(a.minY, b.minY));
|
|
117
133
|
return ix * iy;
|
|
118
134
|
}
|
|
135
|
+
// Fraction of `subject`'s area lying inside any of `covers`, estimated by
|
|
136
|
+
// sampling a grid of cell centers over the subject's bbox. Cheap and robust
|
|
137
|
+
// enough for the merge-vs-demote decision; exact polygon clipping would be a
|
|
138
|
+
// heavy dependency for a 60% threshold.
|
|
139
|
+
function polygonCoverageRatio(subject, covers) {
|
|
140
|
+
if (subject.length < 3 || covers.length === 0)
|
|
141
|
+
return 0;
|
|
142
|
+
const bbox = bboxOf(subject);
|
|
143
|
+
const width = bbox.maxX - bbox.minX;
|
|
144
|
+
const height = bbox.maxY - bbox.minY;
|
|
145
|
+
let inside = 0;
|
|
146
|
+
let covered = 0;
|
|
147
|
+
for (let i = 0; i < COVERAGE_SAMPLE_STEPS; i += 1) {
|
|
148
|
+
for (let j = 0; j < COVERAGE_SAMPLE_STEPS; j += 1) {
|
|
149
|
+
const point = {
|
|
150
|
+
x: bbox.minX + ((i + 0.5) / COVERAGE_SAMPLE_STEPS) * width,
|
|
151
|
+
y: bbox.minY + ((j + 0.5) / COVERAGE_SAMPLE_STEPS) * height,
|
|
152
|
+
};
|
|
153
|
+
if (!pointInPolygon(point, subject))
|
|
154
|
+
continue;
|
|
155
|
+
inside += 1;
|
|
156
|
+
if (pointInAnyPolygon(point, covers))
|
|
157
|
+
covered += 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (inside === 0) {
|
|
161
|
+
return pointInAnyPolygon(polygonCentroid(subject), covers) ? 1 : 0;
|
|
162
|
+
}
|
|
163
|
+
return covered / inside;
|
|
164
|
+
}
|
|
165
|
+
// Demoted auto surfaces keep their polygon untouched, so a re-closed room
|
|
166
|
+
// usually hits the exact-signature manual check. Coverage handles the rest:
|
|
167
|
+
// a room split across multiple manual surfaces AND a single manual surface
|
|
168
|
+
// spanning multiple rooms both suppress a replacement auto surface — what
|
|
169
|
+
// matters is that the ROOM is already substantially covered, not that any
|
|
170
|
+
// one manual surface belongs to it (a per-surface "mostly inside the room"
|
|
171
|
+
// filter dropped multi-room slabs and resurrected deleted auto slabs).
|
|
172
|
+
function matchesManualFootprint(roomPolygon, manualPolygons) {
|
|
173
|
+
return polygonCoverageRatio(roomPolygon, manualPolygons) >= ORPHAN_MERGE_COVERAGE_THRESHOLD;
|
|
174
|
+
}
|
|
119
175
|
function pointDistanceToPolygonBoundary(point, polygon) {
|
|
120
176
|
let minDistance = Number.POSITIVE_INFINITY;
|
|
121
177
|
for (let index = 0; index < polygon.length; index += 1) {
|
|
@@ -144,51 +200,56 @@ function wallBoundsRoom(wall, roomPolygon) {
|
|
|
144
200
|
const matchingPoints = candidates.filter((point) => pointDistanceToPolygonBoundary(point, roomPolygon) <= WALL_ROOM_BOUNDARY_TOLERANCE);
|
|
145
201
|
return matchingPoints.length >= 2;
|
|
146
202
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return true;
|
|
203
|
+
/**
|
|
204
|
+
* The clamp bound for a ceiling polygon under this planning context —
|
|
205
|
+
* the context's cross-level resolver when provided, else the plane-only
|
|
206
|
+
* `storeyHeight - CEILING_CLAMP_MARGIN` degradation.
|
|
207
|
+
*/
|
|
208
|
+
function resolveCeilingClampBound(polygon, context) {
|
|
209
|
+
if (context.ceilingClampBound)
|
|
210
|
+
return context.ceilingClampBound(polygon);
|
|
211
|
+
return (context.storeyHeight ?? DEFAULT_LEVEL_HEIGHT) - CEILING_CLAMP_MARGIN;
|
|
159
212
|
}
|
|
160
|
-
function
|
|
161
|
-
if (
|
|
162
|
-
return
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
213
|
+
function consensusElevation(values) {
|
|
214
|
+
if (values.length === 0 || values.some((value) => !Number.isFinite(value)))
|
|
215
|
+
return undefined;
|
|
216
|
+
const min = Math.min(...values);
|
|
217
|
+
const max = Math.max(...values);
|
|
218
|
+
if (max - min > ROOM_VERTICAL_PLANE_EPSILON)
|
|
219
|
+
return undefined;
|
|
220
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
167
221
|
}
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
222
|
+
function autoRoomVerticalPlacements(spaces, walls, supportSlabs, nodes, storeyHeight) {
|
|
223
|
+
const wallsById = new Map(walls.map((wall) => [wall.id, wall]));
|
|
224
|
+
const placements = new Map();
|
|
225
|
+
for (const space of spaces) {
|
|
226
|
+
const boundaryWalls = space.wallIds.flatMap((id) => {
|
|
227
|
+
const wall = wallsById.get(id);
|
|
228
|
+
return wall ? [wall] : [];
|
|
229
|
+
});
|
|
230
|
+
if (boundaryWalls.length !== space.wallIds.length)
|
|
172
231
|
continue;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
232
|
+
const wallBases = boundaryWalls.map((wall) => {
|
|
233
|
+
const offset = wall.supportOffset ?? 0;
|
|
234
|
+
if (wall.supportSlabId === GROUND_SUPPORT_ID) {
|
|
235
|
+
return ((terrainSupportLift(nodes, space.levelId, wall.start[0], wall.start[1]) ?? 0) + offset);
|
|
236
|
+
}
|
|
237
|
+
return (computeWallSlabSupport(wall, supportSlabs, walls, wall.supportSlabId ?? null).elevation +
|
|
238
|
+
offset);
|
|
239
|
+
});
|
|
240
|
+
const base = consensusElevation(wallBases);
|
|
241
|
+
if (base === undefined)
|
|
181
242
|
continue;
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
243
|
+
const wallTops = boundaryWalls.map((wall, index) => resolveWallTop(wall, storeyHeight, wallBases[index] ?? base));
|
|
244
|
+
const top = consensusElevation(wallTops);
|
|
245
|
+
if (top === undefined)
|
|
246
|
+
continue;
|
|
247
|
+
placements.set(polygonSignature(space.polygon.map(pointFromTuple)), {
|
|
248
|
+
slabElevation: base + DEFAULT_AUTO_SLAB_ELEVATION,
|
|
249
|
+
ceilingHeight: top - CEILING_CLAMP_MARGIN,
|
|
250
|
+
});
|
|
186
251
|
}
|
|
187
|
-
return
|
|
188
|
-
}
|
|
189
|
-
function resolveAutoCeilingHeight(roomPolygon, context = {}) {
|
|
190
|
-
return (resolveRoomSlabElevation(roomPolygon, context.slabs) +
|
|
191
|
-
resolveRoomWallHeight(roomPolygon, context.walls));
|
|
252
|
+
return placements;
|
|
192
253
|
}
|
|
193
254
|
function getWallDirection(wall) {
|
|
194
255
|
const dx = wall.end[0] - wall.start[0];
|
|
@@ -240,11 +301,52 @@ function sampleWallPointsForRoomDetection(wall, tolerance = ROOM_CURVE_TOLERANCE
|
|
|
240
301
|
};
|
|
241
302
|
return subdivide(0, start, 1, end, 0);
|
|
242
303
|
}
|
|
243
|
-
function
|
|
244
|
-
const
|
|
245
|
-
|
|
304
|
+
function segmentProjection(point, start, end) {
|
|
305
|
+
const dx = end.x - start.x;
|
|
306
|
+
const dy = end.y - start.y;
|
|
307
|
+
const lengthSquared = dx * dx + dy * dy;
|
|
308
|
+
if (lengthSquared < 1e-12) {
|
|
309
|
+
return { t: 0, distance: Math.hypot(point.x - start.x, point.y - start.y) };
|
|
310
|
+
}
|
|
311
|
+
const t = ((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSquared;
|
|
312
|
+
const clampedT = Math.max(0, Math.min(1, t));
|
|
313
|
+
const projX = start.x + clampedT * dx;
|
|
314
|
+
const projY = start.y + clampedT * dy;
|
|
315
|
+
return { t, distance: Math.hypot(point.x - projX, point.y - projY) };
|
|
246
316
|
}
|
|
247
|
-
|
|
317
|
+
// Break a straight wall at any junction vertex (another wall's endpoint) that
|
|
318
|
+
// lands on its interior, returning the ordered polyline [start, …splits, end].
|
|
319
|
+
// Splitting at the *vertex* position (not the projection) keeps the split node's
|
|
320
|
+
// key identical to the touching wall's endpoint so the two share a graph node.
|
|
321
|
+
function splitStraightWallAtVertices(start, end, vertices) {
|
|
322
|
+
const length = Math.hypot(end.x - start.x, end.y - start.y);
|
|
323
|
+
if (length < 1e-9)
|
|
324
|
+
return [start, end];
|
|
325
|
+
const interior = [];
|
|
326
|
+
for (const vertex of vertices) {
|
|
327
|
+
const { t, distance } = segmentProjection(vertex, start, end);
|
|
328
|
+
if (distance > WALL_JUNCTION_TOLERANCE)
|
|
329
|
+
continue;
|
|
330
|
+
const along = t * length;
|
|
331
|
+
if (along <= WALL_JUNCTION_TOLERANCE || along >= length - WALL_JUNCTION_TOLERANCE)
|
|
332
|
+
continue;
|
|
333
|
+
interior.push({ point: vertex, t });
|
|
334
|
+
}
|
|
335
|
+
interior.sort((a, b) => a.t - b.t);
|
|
336
|
+
const ordered = [start];
|
|
337
|
+
let lastKey = pointKey(start);
|
|
338
|
+
for (const { point } of interior) {
|
|
339
|
+
const key = pointKey(point);
|
|
340
|
+
if (key === lastKey)
|
|
341
|
+
continue;
|
|
342
|
+
ordered.push(point);
|
|
343
|
+
lastKey = key;
|
|
344
|
+
}
|
|
345
|
+
if (lastKey !== pointKey(end))
|
|
346
|
+
ordered.push(end);
|
|
347
|
+
return ordered;
|
|
348
|
+
}
|
|
349
|
+
function extractRooms(walls) {
|
|
248
350
|
if (walls.length < 3)
|
|
249
351
|
return [];
|
|
250
352
|
const graph = new Map();
|
|
@@ -256,35 +358,71 @@ function extractRoomPolygons(walls) {
|
|
|
256
358
|
}
|
|
257
359
|
return key;
|
|
258
360
|
};
|
|
361
|
+
// Planarize first: collect every wall endpoint as a candidate graph vertex so
|
|
362
|
+
// straight walls can be split at T-junctions where another wall ends mid-span.
|
|
363
|
+
// Without this the touching wall's endpoint is a dangling degree-1 node and the
|
|
364
|
+
// enclosed area (e.g. a room added against the middle of an existing wall)
|
|
365
|
+
// never forms a cycle.
|
|
366
|
+
const vertexByKey = new Map();
|
|
367
|
+
for (const wall of walls) {
|
|
368
|
+
for (const tuple of [wall.start, wall.end]) {
|
|
369
|
+
const point = pointFromTuple(tuple);
|
|
370
|
+
const key = pointKey(point);
|
|
371
|
+
if (!vertexByKey.has(key))
|
|
372
|
+
vertexByKey.set(key, point);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const vertices = [...vertexByKey.values()];
|
|
259
376
|
for (const wall of walls) {
|
|
260
377
|
const start = pointFromTuple(wall.start);
|
|
261
378
|
const end = pointFromTuple(wall.end);
|
|
262
|
-
|
|
263
|
-
const endKey = upsertNode(end);
|
|
264
|
-
if (startKey === endKey)
|
|
379
|
+
if (samePointWithinTolerance(start, end))
|
|
265
380
|
continue;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
381
|
+
// Curved walls keep their sampled polyline as one edge; straight walls split
|
|
382
|
+
// into consecutive sub-edges at their interior junction vertices.
|
|
383
|
+
const subPolylines = isCurvedWall(wall)
|
|
384
|
+
? [sampleWallPointsForRoomDetection(wall)]
|
|
385
|
+
: (() => {
|
|
386
|
+
const ordered = splitStraightWallAtVertices(start, end, vertices);
|
|
387
|
+
const parts = [];
|
|
388
|
+
for (let index = 0; index < ordered.length - 1; index += 1) {
|
|
389
|
+
parts.push([ordered[index], ordered[index + 1]]);
|
|
390
|
+
}
|
|
391
|
+
return parts;
|
|
392
|
+
})();
|
|
393
|
+
subPolylines.forEach((points, subIndex) => {
|
|
394
|
+
const from = points[0];
|
|
395
|
+
const to = points[points.length - 1];
|
|
396
|
+
const fromKey = upsertNode(from);
|
|
397
|
+
const toKey = upsertNode(to);
|
|
398
|
+
if (fromKey === toKey)
|
|
399
|
+
return;
|
|
400
|
+
const reversePoints = [...points].reverse();
|
|
401
|
+
const forwardId = `${wall.id}#${subIndex}:f`;
|
|
402
|
+
const reverseId = `${wall.id}#${subIndex}:r`;
|
|
403
|
+
halfEdges.set(forwardId, {
|
|
404
|
+
id: forwardId,
|
|
405
|
+
reverseId,
|
|
406
|
+
fromKey,
|
|
407
|
+
toKey,
|
|
408
|
+
angle: Math.atan2(points[1].y - from.y, points[1].x - from.x),
|
|
409
|
+
points,
|
|
410
|
+
wallId: wall.id,
|
|
411
|
+
face: 'front',
|
|
412
|
+
});
|
|
413
|
+
halfEdges.set(reverseId, {
|
|
414
|
+
id: reverseId,
|
|
415
|
+
reverseId: forwardId,
|
|
416
|
+
fromKey: toKey,
|
|
417
|
+
toKey: fromKey,
|
|
418
|
+
angle: Math.atan2(reversePoints[1].y - to.y, reversePoints[1].x - to.x),
|
|
419
|
+
points: reversePoints,
|
|
420
|
+
wallId: wall.id,
|
|
421
|
+
face: 'back',
|
|
422
|
+
});
|
|
423
|
+
graph.get(fromKey)?.outgoing.push(forwardId);
|
|
424
|
+
graph.get(toKey)?.outgoing.push(reverseId);
|
|
277
425
|
});
|
|
278
|
-
halfEdges.set(reverseId, {
|
|
279
|
-
id: reverseId,
|
|
280
|
-
reverseId: forwardId,
|
|
281
|
-
fromKey: endKey,
|
|
282
|
-
toKey: startKey,
|
|
283
|
-
angle: Math.atan2(reverseDirection.tangent.y, reverseDirection.tangent.x),
|
|
284
|
-
points: getDirectedWallBoundaryPoints(wall, false),
|
|
285
|
-
});
|
|
286
|
-
graph.get(startKey)?.outgoing.push(forwardId);
|
|
287
|
-
graph.get(endKey)?.outgoing.push(reverseId);
|
|
288
426
|
}
|
|
289
427
|
const sortedOutgoing = new Map();
|
|
290
428
|
for (const [key, node] of graph.entries()) {
|
|
@@ -305,15 +443,49 @@ function extractRoomPolygons(walls) {
|
|
|
305
443
|
const nextIdx = (idx - 1 + outgoing.length) % outgoing.length;
|
|
306
444
|
return outgoing[nextIdx] ?? null;
|
|
307
445
|
};
|
|
446
|
+
const splitIntoSimpleCycles = (walkEdgeIds) => {
|
|
447
|
+
const cycles = [];
|
|
448
|
+
const firstEdge = halfEdges.get(walkEdgeIds[0] ?? '');
|
|
449
|
+
if (!firstEdge)
|
|
450
|
+
return cycles;
|
|
451
|
+
const pathEdges = [];
|
|
452
|
+
const pathVertices = [firstEdge.fromKey];
|
|
453
|
+
const vertexIndex = new Map([[firstEdge.fromKey, 0]]);
|
|
454
|
+
for (const edgeId of walkEdgeIds) {
|
|
455
|
+
const edge = halfEdges.get(edgeId);
|
|
456
|
+
if (!edge || edge.fromKey !== pathVertices[pathVertices.length - 1])
|
|
457
|
+
return [];
|
|
458
|
+
pathEdges.push(edgeId);
|
|
459
|
+
const repeatedIndex = vertexIndex.get(edge.toKey);
|
|
460
|
+
if (repeatedIndex === undefined) {
|
|
461
|
+
pathVertices.push(edge.toKey);
|
|
462
|
+
vertexIndex.set(edge.toKey, pathVertices.length - 1);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const cycle = pathEdges.slice(repeatedIndex);
|
|
466
|
+
if (cycle.length >= 3)
|
|
467
|
+
cycles.push(cycle);
|
|
468
|
+
for (let index = repeatedIndex + 1; index < pathVertices.length; index += 1) {
|
|
469
|
+
vertexIndex.delete(pathVertices[index]);
|
|
470
|
+
}
|
|
471
|
+
pathVertices.length = repeatedIndex + 1;
|
|
472
|
+
pathEdges.length = repeatedIndex;
|
|
473
|
+
}
|
|
474
|
+
return pathEdges.length === 0 && pathVertices.length === 1 ? cycles : [];
|
|
475
|
+
};
|
|
308
476
|
const visitedDirected = new Set();
|
|
309
|
-
const
|
|
310
|
-
|
|
477
|
+
const rooms = [];
|
|
478
|
+
// A face walk cannot revisit a half-edge, so the half-edge count bounds its
|
|
479
|
+
// length. It can revisit a vertex when dangling walls or other graph bridges
|
|
480
|
+
// are traced out and back; those excursions are removed below.
|
|
481
|
+
const maxSteps = Math.min(2000, halfEdges.size + 10);
|
|
311
482
|
for (const edgeId of halfEdges.keys()) {
|
|
312
483
|
if (visitedDirected.has(edgeId))
|
|
313
484
|
continue;
|
|
314
485
|
const cycleEdgeIds = [];
|
|
315
486
|
let currentEdgeId = edgeId;
|
|
316
487
|
let valid = true;
|
|
488
|
+
let closed = false;
|
|
317
489
|
for (let step = 0; step < maxSteps; step += 1) {
|
|
318
490
|
const currentEdge = halfEdges.get(currentEdgeId);
|
|
319
491
|
if (!currentEdge) {
|
|
@@ -328,29 +500,64 @@ function extractRoomPolygons(walls) {
|
|
|
328
500
|
break;
|
|
329
501
|
}
|
|
330
502
|
currentEdgeId = next;
|
|
331
|
-
if (currentEdgeId === edgeId)
|
|
503
|
+
if (currentEdgeId === edgeId) {
|
|
504
|
+
closed = true;
|
|
332
505
|
break;
|
|
506
|
+
}
|
|
333
507
|
}
|
|
334
|
-
if (!valid || cycleEdgeIds.length < 3)
|
|
335
|
-
continue;
|
|
336
|
-
const polygon = dedupeSequentialPoints(cycleEdgeIds.flatMap((id, index) => {
|
|
337
|
-
const points = halfEdges.get(id)?.points ?? [];
|
|
338
|
-
return index === cycleEdgeIds.length - 1 ? points : points.slice(0, -1);
|
|
339
|
-
}));
|
|
340
|
-
if (polygon.length < 3)
|
|
341
|
-
continue;
|
|
342
|
-
const signedArea = polygonArea(polygon);
|
|
343
|
-
if (signedArea <= 0)
|
|
508
|
+
if (!(valid && closed) || cycleEdgeIds.length < 3)
|
|
344
509
|
continue;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
510
|
+
for (const simpleCycleEdgeIds of splitIntoSimpleCycles(cycleEdgeIds)) {
|
|
511
|
+
const polygon = dedupeSequentialPoints(simpleCycleEdgeIds.flatMap((id, index) => {
|
|
512
|
+
const points = halfEdges.get(id)?.points ?? [];
|
|
513
|
+
return index === simpleCycleEdgeIds.length - 1 ? points : points.slice(0, -1);
|
|
514
|
+
}));
|
|
515
|
+
if (polygon.length < 3)
|
|
516
|
+
continue;
|
|
517
|
+
const signedArea = polygonArea(polygon);
|
|
518
|
+
if (signedArea <= 0)
|
|
519
|
+
continue;
|
|
520
|
+
if (signedArea < 0.5 || signedArea > 10_000)
|
|
521
|
+
continue;
|
|
522
|
+
const signature = polygonSignature(polygon);
|
|
523
|
+
if (rooms.some((room) => polygonSignature(room.polygon) === signature))
|
|
524
|
+
continue;
|
|
525
|
+
rooms.push({
|
|
526
|
+
polygon,
|
|
527
|
+
boundaryFaces: simpleCycleEdgeIds.flatMap((id) => {
|
|
528
|
+
const edge = halfEdges.get(id);
|
|
529
|
+
if (!edge)
|
|
530
|
+
return [];
|
|
531
|
+
return [
|
|
532
|
+
{
|
|
533
|
+
wallId: edge.wallId,
|
|
534
|
+
face: edge.face,
|
|
535
|
+
points: edge.points.map(pointToTuple),
|
|
536
|
+
},
|
|
537
|
+
];
|
|
538
|
+
}),
|
|
539
|
+
});
|
|
540
|
+
}
|
|
351
541
|
}
|
|
352
|
-
|
|
353
|
-
return
|
|
542
|
+
rooms.sort((a, b) => Math.abs(polygonArea(b.polygon)) - Math.abs(polygonArea(a.polygon)));
|
|
543
|
+
return rooms;
|
|
544
|
+
}
|
|
545
|
+
function extractRoomPolygons(walls) {
|
|
546
|
+
return extractRooms(walls).map((room) => room.polygon);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* True when `wall` lies on the boundary of a room enclosed by `walls`, using the
|
|
550
|
+
* same planar room graph the auto slab/ceiling sync uses. The wall builder's
|
|
551
|
+
* "Room (auto-close)" mode calls this so drafting stops the moment a segment
|
|
552
|
+
* closes a room — whether the chain loops back to its own start or seals a bay
|
|
553
|
+
* against the middle of an existing wall (a T-junction). Sharing one graph means
|
|
554
|
+
* auto-close and auto-slab detection can never disagree about what is "closed".
|
|
555
|
+
*/
|
|
556
|
+
export function wallClosesRoom(walls, wall) {
|
|
557
|
+
const roomPolygons = extractRoomPolygons(walls);
|
|
558
|
+
if (roomPolygons.length === 0)
|
|
559
|
+
return false;
|
|
560
|
+
return roomPolygons.some((polygon) => wallBoundsRoom(wall, polygon));
|
|
354
561
|
}
|
|
355
562
|
export function resolveWallSurfaceSides(wall, roomPolygons) {
|
|
356
563
|
if (roomPolygons.length === 0) {
|
|
@@ -416,62 +623,147 @@ function wallGeometrySignature(wall) {
|
|
|
416
623
|
wall.end[0].toFixed(4),
|
|
417
624
|
wall.end[1].toFixed(4),
|
|
418
625
|
(wall.thickness ?? 0.2).toFixed(4),
|
|
419
|
-
(
|
|
626
|
+
// Plane-bound (no stored height) is a distinct state, not a default
|
|
627
|
+
// value: it resolves to the storey plane, so it must not alias an
|
|
628
|
+
// explicit height of the same magnitude in the trigger signature.
|
|
629
|
+
wall.height == null ? 'plane' : wall.height.toFixed(4),
|
|
630
|
+
wall.supportSlabId ?? 'elected',
|
|
631
|
+
(wall.supportOffset ?? 0).toFixed(4),
|
|
420
632
|
getClampedWallCurveOffset(wall).toFixed(4),
|
|
421
633
|
].join('|');
|
|
422
634
|
}
|
|
423
635
|
function levelWallSnapshot(walls) {
|
|
424
636
|
return walls.map(wallGeometrySignature).sort().join('||');
|
|
425
637
|
}
|
|
426
|
-
function
|
|
427
|
-
|
|
428
|
-
.
|
|
429
|
-
.
|
|
430
|
-
|
|
431
|
-
.map((
|
|
432
|
-
|
|
433
|
-
return [slab.id, (slab.elevation ?? DEFAULT_AUTO_SLAB_ELEVATION).toFixed(4), polygon, holes].join('|');
|
|
434
|
-
}
|
|
435
|
-
function levelSlabSnapshot(slabs) {
|
|
436
|
-
return slabs.map(slabGeometrySignature).sort().join('||');
|
|
638
|
+
function zoneGeometrySignature(zone) {
|
|
639
|
+
return [
|
|
640
|
+
zone.id,
|
|
641
|
+
zone.autoFromWalls ? 'auto' : 'manual',
|
|
642
|
+
zone.boundaryWallIds.slice().sort().join(','),
|
|
643
|
+
zone.polygon.map(([x, z]) => `${x.toFixed(4)},${z.toFixed(4)}`).join(';'),
|
|
644
|
+
].join('|');
|
|
437
645
|
}
|
|
646
|
+
// Slab/ceiling POLYGONS stay out of the trigger signature: including
|
|
647
|
+
// generated footprints caused delete/recreate feedback. Zones are included
|
|
648
|
+
// only so a newly traced room footprint can adopt its enclosing walls
|
|
649
|
+
// without waiting for the next remodel. Slab ELEVATIONS and the level's
|
|
650
|
+
// stored storey height ARE included — both feed the explicit-ceiling
|
|
651
|
+
// re-clamp bound (the storey plane), and neither is rewritten by
|
|
652
|
+
// the sync, so regeneration triggers when they change without feedback.
|
|
653
|
+
// Stage 3-B adds the LEVEL-ABOVE's covering-slab undersides (elevation −
|
|
654
|
+
// thickness, recessed pools excluded): a deck created, lowered, or
|
|
655
|
+
// thickened above must re-run the sync below so ceilings re-clamp under
|
|
656
|
+
// it. Same polygon exclusion applies — the level-above's own auto sync
|
|
657
|
+
// rewrites its slab footprints, and hashing them here would re-trigger
|
|
658
|
+
// this level on every remodel above.
|
|
438
659
|
function levelStructureSnapshots(nodes) {
|
|
439
|
-
const
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
return entry;
|
|
444
|
-
};
|
|
660
|
+
const wallsByLevel = new Map();
|
|
661
|
+
const zonesByLevel = new Map();
|
|
662
|
+
const slabElevationsByLevel = new Map();
|
|
663
|
+
const coveringUndersidesByLevel = new Map();
|
|
445
664
|
for (const node of Object.values(nodes)) {
|
|
446
665
|
if (!(node && typeof node === 'object' && 'parentId' in node && node.parentId))
|
|
447
666
|
continue;
|
|
667
|
+
const levelId = node.parentId;
|
|
448
668
|
if (node.type === 'wall') {
|
|
449
|
-
|
|
669
|
+
const walls = wallsByLevel.get(levelId) ?? [];
|
|
670
|
+
walls.push(node);
|
|
671
|
+
wallsByLevel.set(levelId, walls);
|
|
672
|
+
}
|
|
673
|
+
else if (node.type === 'zone') {
|
|
674
|
+
const zones = zonesByLevel.get(levelId) ?? [];
|
|
675
|
+
zones.push(ZoneNode.parse(node));
|
|
676
|
+
zonesByLevel.set(levelId, zones);
|
|
450
677
|
}
|
|
451
678
|
else if (node.type === 'slab') {
|
|
452
|
-
|
|
679
|
+
const elevations = slabElevationsByLevel.get(levelId) ?? [];
|
|
680
|
+
elevations.push(`${node.id}:${(node.elevation ?? DEFAULT_AUTO_SLAB_ELEVATION).toFixed(4)}`);
|
|
681
|
+
slabElevationsByLevel.set(levelId, elevations);
|
|
682
|
+
if (node.recessed !== true) {
|
|
683
|
+
const undersides = coveringUndersidesByLevel.get(levelId) ?? [];
|
|
684
|
+
const elevation = node.elevation ?? 0.05;
|
|
685
|
+
const thickness = node.thickness ?? 0.05;
|
|
686
|
+
undersides.push(`${node.id}:${(elevation - thickness).toFixed(4)}`);
|
|
687
|
+
coveringUndersidesByLevel.set(levelId, undersides);
|
|
688
|
+
}
|
|
453
689
|
}
|
|
454
690
|
}
|
|
691
|
+
const levelElevations = getLevelElevations(nodes);
|
|
455
692
|
const snapshots = new Map();
|
|
456
|
-
|
|
457
|
-
|
|
693
|
+
const levelIds = new Set([...wallsByLevel.keys(), ...zonesByLevel.keys()]);
|
|
694
|
+
for (const levelId of levelIds) {
|
|
695
|
+
const walls = wallsByLevel.get(levelId) ?? [];
|
|
696
|
+
const zones = zonesByLevel.get(levelId) ?? [];
|
|
697
|
+
const level = nodes[levelId];
|
|
698
|
+
const storeyKey = level?.type === 'level' && typeof level.height === 'number' ? level.height.toFixed(4) : '';
|
|
699
|
+
const slabKey = (slabElevationsByLevel.get(levelId) ?? []).sort().join(';');
|
|
700
|
+
const aboveId = findLevelAboveId(levelId, levelElevations);
|
|
701
|
+
const aboveSlabKey = aboveId
|
|
702
|
+
? (coveringUndersidesByLevel.get(aboveId) ?? []).sort().join(';')
|
|
703
|
+
: '';
|
|
704
|
+
snapshots.set(levelId, `${storeyKey}#${levelWallSnapshot(walls)}##${zones.map(zoneGeometrySignature).sort().join('||')}##${slabKey}##${aboveSlabKey}`);
|
|
458
705
|
}
|
|
459
706
|
return snapshots;
|
|
460
707
|
}
|
|
461
|
-
function buildSpace(levelId,
|
|
462
|
-
const signature = polygonSignature(polygon);
|
|
708
|
+
function buildSpace(levelId, room) {
|
|
709
|
+
const signature = polygonSignature(room.polygon);
|
|
463
710
|
return {
|
|
464
711
|
id: `space-${levelId}-${signature.slice(0, 12)}`,
|
|
465
712
|
levelId,
|
|
466
|
-
polygon: polygon.map(pointToTuple),
|
|
467
|
-
wallIds: [],
|
|
713
|
+
polygon: room.polygon.map(pointToTuple),
|
|
714
|
+
wallIds: [...new Set(room.boundaryFaces.map((boundary) => boundary.wallId))],
|
|
715
|
+
boundaryFaces: room.boundaryFaces,
|
|
468
716
|
isExterior: false,
|
|
469
717
|
};
|
|
470
718
|
}
|
|
471
|
-
|
|
719
|
+
function sameStringSet(a, b) {
|
|
720
|
+
if (a.length !== b.length)
|
|
721
|
+
return false;
|
|
722
|
+
const right = new Set(b);
|
|
723
|
+
return a.every((value) => right.has(value));
|
|
724
|
+
}
|
|
725
|
+
export function planAutoZonesForLevel(spaces, existingZones) {
|
|
726
|
+
const update = [];
|
|
727
|
+
for (const zone of existingZones) {
|
|
728
|
+
const storedSignature = polygonSignature(zone.polygon.map(pointFromTuple));
|
|
729
|
+
const matchingSpace = zone.autoFromWalls && zone.boundaryWallIds.length >= 3
|
|
730
|
+
? spaces.find((space) => sameStringSet(space.wallIds, zone.boundaryWallIds))
|
|
731
|
+
: spaces.find((space) => polygonSignature(space.polygon.map(pointFromTuple)) === storedSignature);
|
|
732
|
+
if (!matchingSpace)
|
|
733
|
+
continue;
|
|
734
|
+
const data = {};
|
|
735
|
+
if (!zone.autoFromWalls)
|
|
736
|
+
data.autoFromWalls = true;
|
|
737
|
+
if (!sameStringSet(zone.boundaryWallIds, matchingSpace.wallIds)) {
|
|
738
|
+
data.boundaryWallIds = matchingSpace.wallIds;
|
|
739
|
+
}
|
|
740
|
+
if (!sameTuplePolygon(zone.polygon, matchingSpace.polygon)) {
|
|
741
|
+
data.polygon = matchingSpace.polygon;
|
|
742
|
+
}
|
|
743
|
+
if (Object.keys(data).length > 0)
|
|
744
|
+
update.push({ id: zone.id, data });
|
|
745
|
+
}
|
|
746
|
+
return { update };
|
|
747
|
+
}
|
|
748
|
+
export function resolveAutoZonePolygon(zone, resolve) {
|
|
749
|
+
if (!zone.autoFromWalls || zone.boundaryWallIds.length < 3)
|
|
750
|
+
return zone.polygon;
|
|
751
|
+
const walls = zone.boundaryWallIds.flatMap((id) => {
|
|
752
|
+
const node = resolve(id);
|
|
753
|
+
return node && typeof node === 'object' && 'type' in node && node.type === 'wall'
|
|
754
|
+
? [node]
|
|
755
|
+
: [];
|
|
756
|
+
});
|
|
757
|
+
if (walls.length !== zone.boundaryWallIds.length)
|
|
758
|
+
return zone.polygon;
|
|
759
|
+
const room = extractRooms(walls).find((candidate) => sameStringSet([...new Set(candidate.boundaryFaces.map((boundary) => boundary.wallId))], zone.boundaryWallIds));
|
|
760
|
+
return room ? room.polygon.map(pointToTuple) : zone.polygon;
|
|
761
|
+
}
|
|
762
|
+
export function planAutoSlabsForLevel(roomPolygons, existingSlabs, context = {}) {
|
|
472
763
|
const manualSlabs = existingSlabs.filter((slab) => !slab.autoFromWalls);
|
|
473
764
|
const manualSignatures = new Set(manualSlabs.map((slab) => polygonSignature(slab.polygon.map(pointFromTuple))));
|
|
474
|
-
const
|
|
765
|
+
const manualPolygons = manualSlabs.map((slab) => slab.polygon.map(pointFromTuple));
|
|
766
|
+
const detectedAll = roomPolygons
|
|
475
767
|
.map((poly) => ({
|
|
476
768
|
poly: simplifyClosedPolygon(poly.map(pointToTuple), AUTO_SLAB_POLYGON_SIMPLIFY_TOLERANCE).map(pointFromTuple),
|
|
477
769
|
sig: '',
|
|
@@ -485,8 +777,8 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
|
|
|
485
777
|
centroid: polygonCentroid(room.poly),
|
|
486
778
|
area: Math.abs(polygonArea(room.poly)),
|
|
487
779
|
bbox: bboxOf(room.poly),
|
|
488
|
-
}))
|
|
489
|
-
|
|
780
|
+
}));
|
|
781
|
+
const detected = detectedAll.filter(({ sig, poly }) => !manualSignatures.has(sig) && !matchesManualFootprint(poly, manualPolygons));
|
|
490
782
|
const existingAuto = existingSlabs.filter((slab) => slab.autoFromWalls);
|
|
491
783
|
const existingAutoMeta = existingAuto.map((slab) => {
|
|
492
784
|
const poly = slab.polygon.map(pointFromTuple);
|
|
@@ -503,15 +795,21 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
|
|
|
503
795
|
const updatesById = new Map();
|
|
504
796
|
const autoBySignature = new Map();
|
|
505
797
|
for (const entry of existingAutoMeta) {
|
|
506
|
-
autoBySignature.
|
|
798
|
+
const bucket = autoBySignature.get(entry.sig) ?? [];
|
|
799
|
+
bucket.push(entry);
|
|
800
|
+
autoBySignature.set(entry.sig, bucket);
|
|
507
801
|
}
|
|
508
802
|
detected.forEach((room, index) => {
|
|
509
|
-
const existing = autoBySignature.get(room.sig);
|
|
803
|
+
const existing = autoBySignature.get(room.sig)?.shift();
|
|
510
804
|
if (!existing)
|
|
511
805
|
return;
|
|
512
806
|
matchedDetectedIdx.add(index);
|
|
513
807
|
matchedSlabIds.add(existing.slab.id);
|
|
514
|
-
|
|
808
|
+
const polygon = room.poly.map(pointToTuple);
|
|
809
|
+
updatesById.set(existing.slab.id, {
|
|
810
|
+
polygon,
|
|
811
|
+
elevation: context.elevationForRoom?.(polygon),
|
|
812
|
+
});
|
|
515
813
|
});
|
|
516
814
|
const remainingDetected = detected
|
|
517
815
|
.map((room, index) => ({ room, index }))
|
|
@@ -540,19 +838,46 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
|
|
|
540
838
|
continue;
|
|
541
839
|
matchedDetectedIdx.add(index);
|
|
542
840
|
matchedSlabIds.add(bestMatch.entry.slab.id);
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
841
|
+
const polygon = room.poly.map(pointToTuple);
|
|
842
|
+
updatesById.set(bestMatch.entry.slab.id, {
|
|
843
|
+
polygon,
|
|
844
|
+
elevation: context.elevationForRoom?.(polygon),
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
const detectedRoomPolygons = detectedAll.map((room) => room.poly);
|
|
848
|
+
const slabsToDelete = [];
|
|
849
|
+
const slabDemotions = [];
|
|
850
|
+
for (const slab of existingAuto) {
|
|
851
|
+
if (updatesById.has(slab.id))
|
|
852
|
+
continue;
|
|
853
|
+
const coverage = polygonCoverageRatio(slab.polygon.map(pointFromTuple), detectedRoomPolygons);
|
|
854
|
+
if (coverage >= ORPHAN_MERGE_COVERAGE_THRESHOLD) {
|
|
855
|
+
slabsToDelete.push(slab.id);
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
// Render offsets derive from level context at geometry build time, so
|
|
859
|
+
// demotion leaves the stored polygon untouched (same as ceilings).
|
|
860
|
+
slabDemotions.push({ id: slab.id, data: { autoFromWalls: false } });
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const slabsToUpdate = [
|
|
864
|
+
...existingAuto
|
|
865
|
+
.filter((slab) => updatesById.has(slab.id))
|
|
866
|
+
.flatMap((slab) => {
|
|
867
|
+
const update = updatesById.get(slab.id);
|
|
868
|
+
if (!update)
|
|
869
|
+
return [];
|
|
870
|
+
const data = {};
|
|
871
|
+
if (!sameTuplePolygon(slab.polygon, update.polygon))
|
|
872
|
+
data.polygon = update.polygon;
|
|
873
|
+
if (update.elevation !== undefined &&
|
|
874
|
+
Math.abs(slab.elevation - update.elevation) > ROOM_VERTICAL_PLANE_EPSILON) {
|
|
875
|
+
data.elevation = update.elevation;
|
|
876
|
+
}
|
|
877
|
+
return Object.keys(data).length > 0 ? [{ id: slab.id, data }] : [];
|
|
878
|
+
}),
|
|
879
|
+
...slabDemotions,
|
|
880
|
+
];
|
|
556
881
|
const plannedSlabsForNaming = [...existingSlabs];
|
|
557
882
|
const slabsToCreate = [];
|
|
558
883
|
for (let index = 0; index < detected.length; index += 1) {
|
|
@@ -563,11 +888,15 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
|
|
|
563
888
|
continue;
|
|
564
889
|
const name = nextAutoRoomName(plannedSlabsForNaming, 'Slab');
|
|
565
890
|
plannedSlabsForNaming.push({ name });
|
|
891
|
+
const polygon = room.poly.map(pointToTuple);
|
|
892
|
+
const elevation = context.elevationForRoom?.(polygon);
|
|
566
893
|
slabsToCreate.push(SlabNode.parse({
|
|
567
894
|
name,
|
|
568
|
-
polygon
|
|
895
|
+
polygon,
|
|
569
896
|
holes: [],
|
|
570
|
-
elevation:
|
|
897
|
+
elevation: elevation !== undefined && Number.isFinite(elevation)
|
|
898
|
+
? elevation
|
|
899
|
+
: DEFAULT_AUTO_SLAB_ELEVATION,
|
|
571
900
|
autoFromWalls: true,
|
|
572
901
|
}));
|
|
573
902
|
}
|
|
@@ -577,8 +906,8 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
|
|
|
577
906
|
delete: slabsToDelete,
|
|
578
907
|
};
|
|
579
908
|
}
|
|
580
|
-
function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore) {
|
|
581
|
-
const plan = planAutoSlabsForLevel(roomPolygons, existingSlabs);
|
|
909
|
+
function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore, context = {}) {
|
|
910
|
+
const plan = planAutoSlabsForLevel(roomPolygons, existingSlabs, context);
|
|
582
911
|
if (plan.delete.length > 0) {
|
|
583
912
|
sceneStore.getState().deleteNodes(plan.delete);
|
|
584
913
|
}
|
|
@@ -590,26 +919,11 @@ function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore)
|
|
|
590
919
|
}
|
|
591
920
|
return plan;
|
|
592
921
|
}
|
|
593
|
-
export function projectAutoSlabsForPlan(existingSlabs, plan) {
|
|
594
|
-
const slabsById = new Map(existingSlabs.map((slab) => [slab.id, slab]));
|
|
595
|
-
for (const id of plan.delete) {
|
|
596
|
-
slabsById.delete(id);
|
|
597
|
-
}
|
|
598
|
-
for (const update of plan.update) {
|
|
599
|
-
const slab = slabsById.get(update.id);
|
|
600
|
-
if (!slab)
|
|
601
|
-
continue;
|
|
602
|
-
slabsById.set(update.id, SlabNode.parse({ ...slab, ...update.data }));
|
|
603
|
-
}
|
|
604
|
-
for (const slab of plan.create) {
|
|
605
|
-
slabsById.set(slab.id, slab);
|
|
606
|
-
}
|
|
607
|
-
return [...slabsById.values()];
|
|
608
|
-
}
|
|
609
922
|
export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context = {}) {
|
|
610
923
|
const manualCeilings = existingCeilings.filter((ceiling) => !ceiling.autoFromWalls);
|
|
611
924
|
const manualSignatures = new Set(manualCeilings.map((ceiling) => polygonSignature(ceiling.polygon.map(pointFromTuple))));
|
|
612
|
-
const
|
|
925
|
+
const manualPolygons = manualCeilings.map((ceiling) => ceiling.polygon.map(pointFromTuple));
|
|
926
|
+
const detectedAll = roomPolygons
|
|
613
927
|
.map((poly) => ({
|
|
614
928
|
poly: simplifyClosedPolygon(poly.map(pointToTuple), AUTO_SLAB_POLYGON_SIMPLIFY_TOLERANCE).map(pointFromTuple),
|
|
615
929
|
sig: '',
|
|
@@ -623,9 +937,8 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
|
|
|
623
937
|
centroid: polygonCentroid(room.poly),
|
|
624
938
|
area: Math.abs(polygonArea(room.poly)),
|
|
625
939
|
bbox: bboxOf(room.poly),
|
|
626
|
-
|
|
627
|
-
}))
|
|
628
|
-
.filter(({ sig }) => !manualSignatures.has(sig));
|
|
940
|
+
}));
|
|
941
|
+
const detected = detectedAll.filter(({ sig, poly }) => !manualSignatures.has(sig) && !matchesManualFootprint(poly, manualPolygons));
|
|
629
942
|
const existingAuto = existingCeilings.filter((ceiling) => ceiling.autoFromWalls);
|
|
630
943
|
const existingAutoMeta = existingAuto.map((ceiling) => {
|
|
631
944
|
const poly = ceiling.polygon.map(pointFromTuple);
|
|
@@ -642,17 +955,20 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
|
|
|
642
955
|
const updatesById = new Map();
|
|
643
956
|
const autoBySignature = new Map();
|
|
644
957
|
for (const entry of existingAutoMeta) {
|
|
645
|
-
autoBySignature.
|
|
958
|
+
const bucket = autoBySignature.get(entry.sig) ?? [];
|
|
959
|
+
bucket.push(entry);
|
|
960
|
+
autoBySignature.set(entry.sig, bucket);
|
|
646
961
|
}
|
|
647
962
|
detected.forEach((room, index) => {
|
|
648
|
-
const existing = autoBySignature.get(room.sig);
|
|
963
|
+
const existing = autoBySignature.get(room.sig)?.shift();
|
|
649
964
|
if (!existing)
|
|
650
965
|
return;
|
|
651
966
|
matchedDetectedIdx.add(index);
|
|
652
967
|
matchedCeilingIds.add(existing.ceiling.id);
|
|
968
|
+
const polygon = room.poly.map(pointToTuple);
|
|
653
969
|
updatesById.set(existing.ceiling.id, {
|
|
654
|
-
polygon
|
|
655
|
-
height:
|
|
970
|
+
polygon,
|
|
971
|
+
height: context.heightForRoom?.(polygon),
|
|
656
972
|
});
|
|
657
973
|
});
|
|
658
974
|
const remainingDetected = detected
|
|
@@ -682,30 +998,64 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
|
|
|
682
998
|
continue;
|
|
683
999
|
matchedDetectedIdx.add(index);
|
|
684
1000
|
matchedCeilingIds.add(bestMatch.entry.ceiling.id);
|
|
1001
|
+
const polygon = room.poly.map(pointToTuple);
|
|
685
1002
|
updatesById.set(bestMatch.entry.ceiling.id, {
|
|
686
|
-
polygon
|
|
687
|
-
height:
|
|
1003
|
+
polygon,
|
|
1004
|
+
height: context.heightForRoom?.(polygon),
|
|
688
1005
|
});
|
|
689
1006
|
}
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const
|
|
697
|
-
if (
|
|
698
|
-
|
|
699
|
-
const data = {};
|
|
700
|
-
if (!sameTuplePolygon(ceiling.polygon, update.polygon)) {
|
|
701
|
-
data.polygon = update.polygon;
|
|
1007
|
+
const detectedRoomPolygons = detectedAll.map((room) => room.poly);
|
|
1008
|
+
const ceilingsToDelete = [];
|
|
1009
|
+
const ceilingDemotions = [];
|
|
1010
|
+
for (const ceiling of existingAuto) {
|
|
1011
|
+
if (updatesById.has(ceiling.id))
|
|
1012
|
+
continue;
|
|
1013
|
+
const coverage = polygonCoverageRatio(ceiling.polygon.map(pointFromTuple), detectedRoomPolygons);
|
|
1014
|
+
if (coverage >= ORPHAN_MERGE_COVERAGE_THRESHOLD) {
|
|
1015
|
+
ceilingsToDelete.push(ceiling.id);
|
|
702
1016
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
data.height = update.height;
|
|
1017
|
+
else {
|
|
1018
|
+
ceilingDemotions.push({ id: ceiling.id, data: { autoFromWalls: false } });
|
|
706
1019
|
}
|
|
707
|
-
|
|
1020
|
+
}
|
|
1021
|
+
// Stage 3-B reactive re-clamp (clamp-never-ask): a covering slab
|
|
1022
|
+
// created, moved, or thickened on the level above can leave an EXISTING
|
|
1023
|
+
// manual explicit-height ceiling poking into its solid. Clamp explicit
|
|
1024
|
+
// heights down to the bound; never raise them — a user-lowered ceiling
|
|
1025
|
+
// is intent, only an over-bound one is a conflict. Follows-mode
|
|
1026
|
+
// ceilings (absent height) derive under the bound by construction and
|
|
1027
|
+
// are skipped, so the clamp can never convert one to an explicit
|
|
1028
|
+
// height.
|
|
1029
|
+
const manualClamps = manualCeilings.flatMap((ceiling) => {
|
|
1030
|
+
if (ceiling.height == null)
|
|
1031
|
+
return [];
|
|
1032
|
+
const bound = resolveCeilingClampBound(ceiling.polygon, context);
|
|
1033
|
+
if (!Number.isFinite(bound))
|
|
1034
|
+
return [];
|
|
1035
|
+
return ceiling.height > bound + CEILING_HEIGHT_EPSILON
|
|
1036
|
+
? [{ id: ceiling.id, data: { height: bound } }]
|
|
1037
|
+
: [];
|
|
708
1038
|
});
|
|
1039
|
+
const ceilingsToUpdate = [
|
|
1040
|
+
...existingAuto
|
|
1041
|
+
.filter((ceiling) => updatesById.has(ceiling.id))
|
|
1042
|
+
.flatMap((ceiling) => {
|
|
1043
|
+
const update = updatesById.get(ceiling.id);
|
|
1044
|
+
if (!update)
|
|
1045
|
+
return [];
|
|
1046
|
+
const data = {};
|
|
1047
|
+
if (!sameTuplePolygon(ceiling.polygon, update.polygon))
|
|
1048
|
+
data.polygon = update.polygon;
|
|
1049
|
+
if (update.height !== undefined &&
|
|
1050
|
+
(ceiling.height === undefined ||
|
|
1051
|
+
Math.abs(ceiling.height - update.height) > ROOM_VERTICAL_PLANE_EPSILON)) {
|
|
1052
|
+
data.height = update.height;
|
|
1053
|
+
}
|
|
1054
|
+
return Object.keys(data).length > 0 ? [{ id: ceiling.id, data }] : [];
|
|
1055
|
+
}),
|
|
1056
|
+
...ceilingDemotions,
|
|
1057
|
+
...manualClamps,
|
|
1058
|
+
];
|
|
709
1059
|
const plannedCeilingsForNaming = [...existingCeilings];
|
|
710
1060
|
const ceilingsToCreate = [];
|
|
711
1061
|
for (let index = 0; index < detected.length; index += 1) {
|
|
@@ -716,11 +1066,13 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
|
|
|
716
1066
|
continue;
|
|
717
1067
|
const name = nextAutoRoomName(plannedCeilingsForNaming, 'Ceiling');
|
|
718
1068
|
plannedCeilingsForNaming.push({ name });
|
|
1069
|
+
const polygon = room.poly.map(pointToTuple);
|
|
1070
|
+
const height = context.heightForRoom?.(polygon);
|
|
719
1071
|
ceilingsToCreate.push(CeilingNode.parse({
|
|
720
1072
|
name,
|
|
721
|
-
polygon
|
|
1073
|
+
polygon,
|
|
722
1074
|
holes: [],
|
|
723
|
-
height:
|
|
1075
|
+
...(height !== undefined && Number.isFinite(height) ? { height } : {}),
|
|
724
1076
|
autoFromWalls: true,
|
|
725
1077
|
}));
|
|
726
1078
|
}
|
|
@@ -743,14 +1095,15 @@ function syncAutoCeilingsForLevel(levelId, roomPolygons, existingCeilings, scene
|
|
|
743
1095
|
}
|
|
744
1096
|
}
|
|
745
1097
|
function detectSpacesFromWalls(levelId, walls) {
|
|
746
|
-
const
|
|
1098
|
+
const rooms = extractRooms(walls);
|
|
1099
|
+
const roomPolygons = rooms.map((room) => room.polygon);
|
|
747
1100
|
const wallUpdates = walls.map((wall) => ({
|
|
748
1101
|
wallId: wall.id,
|
|
749
1102
|
...resolveWallSurfaceSides(wall, roomPolygons),
|
|
750
1103
|
}));
|
|
751
1104
|
return {
|
|
752
1105
|
roomPolygons,
|
|
753
|
-
spaces:
|
|
1106
|
+
spaces: rooms.map((room) => buildSpace(levelId, room)),
|
|
754
1107
|
wallUpdates,
|
|
755
1108
|
};
|
|
756
1109
|
}
|
|
@@ -770,6 +1123,7 @@ function runSpaceDetection(levelIds, sceneStore, editorStore, nodes) {
|
|
|
770
1123
|
const walls = Object.values(nodes).filter((node) => node?.type === 'wall' && node.parentId === levelId);
|
|
771
1124
|
const slabs = Object.values(nodes).filter((node) => node?.type === 'slab' && node.parentId === levelId);
|
|
772
1125
|
const ceilings = Object.values(nodes).filter((node) => node?.type === 'ceiling' && node.parentId === levelId);
|
|
1126
|
+
const zones = Object.values(nodes).filter((node) => node?.type === 'zone' && node.parentId === levelId);
|
|
773
1127
|
const { wallUpdates, spaces, roomPolygons } = detectSpacesFromWalls(levelId, walls);
|
|
774
1128
|
const changedWallUpdates = wallUpdates.filter((update) => {
|
|
775
1129
|
const wall = nodes[update.wallId];
|
|
@@ -784,10 +1138,27 @@ function runSpaceDetection(levelIds, sceneStore, editorStore, nodes) {
|
|
|
784
1138
|
},
|
|
785
1139
|
})));
|
|
786
1140
|
}
|
|
1141
|
+
const levelNode = nodes[levelId];
|
|
1142
|
+
const storeyHeight = levelNode?.type === 'level'
|
|
1143
|
+
? getStoredLevelHeight(levelNode)
|
|
1144
|
+
: DEFAULT_LEVEL_HEIGHT;
|
|
787
1145
|
const parsedSlabs = slabs.map((slab) => SlabNode.parse(slab));
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
1146
|
+
const verticalPlacements = autoRoomVerticalPlacements(spaces, walls,
|
|
1147
|
+
// A derived floor cannot be evidence for its own next elevation: that
|
|
1148
|
+
// would lift unpinned walls, then lift the floor again on every pass.
|
|
1149
|
+
parsedSlabs.filter((slab) => !slab.autoFromWalls), nodes, storeyHeight);
|
|
1150
|
+
const placementFor = (polygon) => verticalPlacements.get(polygonSignature(polygon.map(pointFromTuple)));
|
|
1151
|
+
syncAutoSlabsForLevel(levelId, roomPolygons, parsedSlabs, sceneStore, {
|
|
1152
|
+
elevationForRoom: (polygon) => placementFor(polygon)?.slabElevation,
|
|
1153
|
+
});
|
|
1154
|
+
syncAutoCeilingsForLevel(levelId, roomPolygons, ceilings.map((ceiling) => CeilingNode.parse(ceiling)), sceneStore, {
|
|
1155
|
+
storeyHeight,
|
|
1156
|
+
ceilingClampBound: (polygon) => getCeilingClampBound(levelId, nodes, polygon),
|
|
1157
|
+
heightForRoom: (polygon) => placementFor(polygon)?.ceilingHeight,
|
|
1158
|
+
});
|
|
1159
|
+
const zonePlan = planAutoZonesForLevel(spaces, zones.map((zone) => ZoneNode.parse(zone)));
|
|
1160
|
+
if (zonePlan.update.length > 0)
|
|
1161
|
+
updateNodes(zonePlan.update);
|
|
791
1162
|
for (const space of spaces) {
|
|
792
1163
|
nextSpaces[space.id] = space;
|
|
793
1164
|
}
|
|
@@ -815,7 +1186,11 @@ export function isSpaceDetectionPaused() {
|
|
|
815
1186
|
return spaceDetectionPauseDepth > 0;
|
|
816
1187
|
}
|
|
817
1188
|
export function initSpaceDetectionSync(sceneStore, editorStore) {
|
|
818
|
-
|
|
1189
|
+
// Baseline from whatever is already in the store. Detection reacts to wall
|
|
1190
|
+
// edits made IN-SESSION (create / move / delete); it must not re-litigate a
|
|
1191
|
+
// scene that merely loaded — rerunning on hydration resurrected auto slabs
|
|
1192
|
+
// the user had deleted in an earlier session.
|
|
1193
|
+
const previousSnapshots = levelStructureSnapshots(sceneStore.getState().nodes);
|
|
819
1194
|
let isProcessing = false;
|
|
820
1195
|
const unsubscribe = sceneStore.subscribe((state) => {
|
|
821
1196
|
if (isProcessing)
|
|
@@ -836,7 +1211,14 @@ export function initSpaceDetectionSync(sceneStore, editorStore) {
|
|
|
836
1211
|
}
|
|
837
1212
|
const levelsToUpdate = new Set();
|
|
838
1213
|
for (const levelId of new Set([...previousSnapshots.keys(), ...currentSnapshots.keys()])) {
|
|
839
|
-
|
|
1214
|
+
// First sight of a level is a hydration baseline, not a wall edit —
|
|
1215
|
+
// `setScene` delivers a loaded scene as one atomic update, and a level's
|
|
1216
|
+
// first wall can't close a room anyway. Record it (below) and only
|
|
1217
|
+
// react to subsequent changes.
|
|
1218
|
+
const previous = previousSnapshots.get(levelId);
|
|
1219
|
+
if (previous === undefined)
|
|
1220
|
+
continue;
|
|
1221
|
+
if (previous !== (currentSnapshots.get(levelId) ?? '')) {
|
|
840
1222
|
levelsToUpdate.add(levelId);
|
|
841
1223
|
}
|
|
842
1224
|
}
|