@pascal-app/core 0.8.0 → 0.9.2
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/dist/events/bus.d.ts +67 -13
- 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 +14 -23
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.js +59 -26
- package/dist/hooks/scene-registry/scene-registry.test.d.ts +2 -0
- package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +1 -0
- package/dist/hooks/scene-registry/scene-registry.test.js +26 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +14 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.js +51 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +2 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +366 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +79 -11
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +508 -141
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +28 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +109 -18
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +2 -0
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +363 -0
- package/dist/index.d.ts +36 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -6
- 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 +19 -0
- package/dist/lib/measurement-geometry.d.ts.map +1 -0
- package/dist/lib/measurement-geometry.js +232 -0
- package/dist/lib/measurement-geometry.test.d.ts +2 -0
- package/dist/lib/measurement-geometry.test.d.ts.map +1 -0
- package/dist/lib/measurement-geometry.test.js +102 -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/polygon-relations.d.ts +10 -0
- package/dist/lib/polygon-relations.d.ts.map +1 -0
- package/dist/lib/polygon-relations.js +73 -0
- 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 +525 -31
- package/dist/lib/slab-polygon.test.d.ts +2 -0
- package/dist/lib/slab-polygon.test.d.ts.map +1 -0
- package/dist/lib/slab-polygon.test.js +635 -0
- 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-pause.test.d.ts +2 -0
- package/dist/lib/space-detection-pause.test.d.ts.map +1 -0
- package/dist/lib/space-detection-pause.test.js +42 -0
- package/dist/lib/space-detection.d.ts +54 -2
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +599 -124
- package/dist/lib/space-detection.test.d.ts +2 -0
- package/dist/lib/space-detection.test.d.ts.map +1 -0
- package/dist/lib/space-detection.test.js +376 -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 +183 -0
- package/dist/lib/zone-quantities.test.d.ts +2 -0
- package/dist/lib/zone-quantities.test.d.ts.map +1 -0
- package/dist/lib/zone-quantities.test.js +82 -0
- package/dist/material-library.d.ts +20 -1
- package/dist/material-library.d.ts.map +1 -1
- package/dist/material-library.js +3737 -200
- package/dist/registry/__bench__/relations-resolver.bench.d.ts +15 -0
- package/dist/registry/__bench__/relations-resolver.bench.d.ts.map +1 -0
- package/dist/registry/__bench__/relations-resolver.bench.js +170 -0
- package/dist/registry/handles.d.ts +394 -0
- package/dist/registry/handles.d.ts.map +1 -0
- package/dist/registry/handles.js +24 -0
- package/dist/registry/index.d.ts +7 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +4 -0
- package/dist/registry/registry.d.ts +136 -0
- package/dist/registry/registry.d.ts.map +1 -0
- package/dist/registry/registry.js +282 -0
- package/dist/registry/registry.test.d.ts +2 -0
- package/dist/registry/registry.test.d.ts.map +1 -0
- package/dist/registry/registry.test.js +196 -0
- package/dist/registry/relations-resolver.d.ts +42 -0
- package/dist/registry/relations-resolver.d.ts.map +1 -0
- package/dist/registry/relations-resolver.js +77 -0
- package/dist/registry/relations-resolver.test.d.ts +2 -0
- package/dist/registry/relations-resolver.test.d.ts.map +1 -0
- package/dist/registry/relations-resolver.test.js +183 -0
- package/dist/registry/scene-api.d.ts +44 -0
- package/dist/registry/scene-api.d.ts.map +1 -0
- package/dist/registry/scene-api.js +111 -0
- package/dist/registry/scene-api.test.d.ts +2 -0
- package/dist/registry/scene-api.test.d.ts.map +1 -0
- package/dist/registry/scene-api.test.js +183 -0
- package/dist/registry/subtree.d.ts +67 -0
- package/dist/registry/subtree.d.ts.map +1 -0
- package/dist/registry/subtree.js +123 -0
- package/dist/registry/subtree.test.d.ts +2 -0
- package/dist/registry/subtree.test.d.ts.map +1 -0
- package/dist/registry/subtree.test.js +163 -0
- package/dist/registry/types.d.ts +2099 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/types.js +1 -0
- package/dist/schema/index.d.ts +35 -4
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +32 -4
- package/dist/schema/material.d.ts +13 -2
- package/dist/schema/material.d.ts.map +1 -1
- package/dist/schema/material.js +17 -5
- package/dist/schema/material.test.d.ts +2 -0
- package/dist/schema/material.test.d.ts.map +1 -0
- package/dist/schema/material.test.js +43 -0
- package/dist/schema/nodes/box-vent.d.ts +74 -0
- package/dist/schema/nodes/box-vent.d.ts.map +1 -0
- package/dist/schema/nodes/box-vent.js +63 -0
- package/dist/schema/nodes/building.d.ts +1 -1
- package/dist/schema/nodes/building.d.ts.map +1 -1
- package/dist/schema/nodes/building.js +3 -2
- package/dist/schema/nodes/cabinet.d.ts +534 -0
- package/dist/schema/nodes/cabinet.d.ts.map +1 -0
- package/dist/schema/nodes/cabinet.js +137 -0
- package/dist/schema/nodes/ceiling.d.ts +5 -2
- package/dist/schema/nodes/ceiling.d.ts.map +1 -1
- package/dist/schema/nodes/ceiling.js +5 -1
- package/dist/schema/nodes/chimney.d.ts +158 -0
- package/dist/schema/nodes/chimney.d.ts.map +1 -0
- package/dist/schema/nodes/chimney.js +67 -0
- package/dist/schema/nodes/column.d.ts +600 -7
- package/dist/schema/nodes/column.d.ts.map +1 -1
- package/dist/schema/nodes/column.js +589 -6
- package/dist/schema/nodes/cupola.d.ts +66 -0
- package/dist/schema/nodes/cupola.d.ts.map +1 -0
- package/dist/schema/nodes/cupola.js +28 -0
- package/dist/schema/nodes/door.d.ts +13 -5
- package/dist/schema/nodes/door.d.ts.map +1 -1
- package/dist/schema/nodes/door.js +12 -0
- package/dist/schema/nodes/dormer.d.ts +228 -0
- package/dist/schema/nodes/dormer.d.ts.map +1 -0
- package/dist/schema/nodes/dormer.js +119 -0
- package/dist/schema/nodes/downspout.d.ts +75 -0
- package/dist/schema/nodes/downspout.d.ts.map +1 -0
- package/dist/schema/nodes/downspout.js +63 -0
- 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 +57 -0
- package/dist/schema/nodes/duct-terminal.d.ts.map +1 -0
- package/dist/schema/nodes/duct-terminal.js +51 -0
- package/dist/schema/nodes/elevator.d.ts +106 -0
- package/dist/schema/nodes/elevator.d.ts.map +1 -0
- package/dist/schema/nodes/elevator.js +57 -0
- package/dist/schema/nodes/eyebrow-vent.d.ts +68 -0
- package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -0
- package/dist/schema/nodes/eyebrow-vent.js +40 -0
- package/dist/schema/nodes/fence.d.ts +19 -2
- package/dist/schema/nodes/fence.d.ts.map +1 -1
- package/dist/schema/nodes/fence.js +27 -2
- package/dist/schema/nodes/gutter.d.ts +84 -0
- package/dist/schema/nodes/gutter.d.ts.map +1 -0
- package/dist/schema/nodes/gutter.js +76 -0
- package/dist/schema/nodes/hvac-equipment.d.ts +63 -0
- package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -0
- package/dist/schema/nodes/hvac-equipment.js +55 -0
- package/dist/schema/nodes/item.d.ts +12 -0
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +20 -0
- package/dist/schema/nodes/level.d.ts +1 -1
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +4 -0
- 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/measurement.test.d.ts +2 -0
- package/dist/schema/nodes/measurement.test.d.ts.map +1 -0
- package/dist/schema/nodes/measurement.test.js +139 -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-defaults.test.d.ts +2 -0
- package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +1 -0
- package/dist/schema/nodes/ridge-vent-defaults.test.js +205 -0
- package/dist/schema/nodes/ridge-vent.d.ts +82 -0
- package/dist/schema/nodes/ridge-vent.d.ts.map +1 -0
- package/dist/schema/nodes/ridge-vent.js +248 -0
- 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-shape.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-shape.test.js +175 -0
- package/dist/schema/nodes/roof-segment-surface.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-surface.test.js +48 -0
- package/dist/schema/nodes/roof-segment-trim.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-trim.test.js +107 -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-walls.test.d.ts +2 -0
- package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +1 -0
- package/dist/schema/nodes/roof-segment-walls.test.js +92 -0
- package/dist/schema/nodes/roof-segment.d.ts +273 -3
- package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment.js +539 -3
- package/dist/schema/nodes/roof.d.ts +8 -8
- package/dist/schema/nodes/roof.d.ts.map +1 -1
- package/dist/schema/nodes/roof.js +4 -16
- package/dist/schema/nodes/shelf.d.ts +106 -0
- package/dist/schema/nodes/shelf.d.ts.map +1 -0
- package/dist/schema/nodes/shelf.js +90 -0
- package/dist/schema/nodes/site.d.ts +1 -122
- package/dist/schema/nodes/site.d.ts.map +1 -1
- package/dist/schema/nodes/site.js +2 -6
- package/dist/schema/nodes/skylight.d.ts +279 -0
- package/dist/schema/nodes/skylight.d.ts.map +1 -0
- package/dist/schema/nodes/skylight.js +152 -0
- package/dist/schema/nodes/slab.d.ts +5 -2
- package/dist/schema/nodes/slab.d.ts.map +1 -1
- package/dist/schema/nodes/slab.js +5 -1
- package/dist/schema/nodes/solar-panel.d.ts +117 -0
- package/dist/schema/nodes/solar-panel.d.ts.map +1 -0
- package/dist/schema/nodes/solar-panel.js +47 -0
- package/dist/schema/nodes/stair-segment.d.ts +4 -4
- package/dist/schema/nodes/stair.d.ts +13 -12
- package/dist/schema/nodes/stair.d.ts.map +1 -1
- package/dist/schema/nodes/stair.js +3 -0
- package/dist/schema/nodes/surface-hole-metadata.d.ts +2 -0
- package/dist/schema/nodes/surface-hole-metadata.d.ts.map +1 -1
- package/dist/schema/nodes/surface-hole-metadata.js +3 -1
- package/dist/schema/nodes/turbine-vent.d.ts +68 -0
- package/dist/schema/nodes/turbine-vent.d.ts.map +1 -0
- package/dist/schema/nodes/turbine-vent.js +41 -0
- package/dist/schema/nodes/wall.d.ts +245 -6
- package/dist/schema/nodes/wall.d.ts.map +1 -1
- package/dist/schema/nodes/wall.js +257 -0
- package/dist/schema/nodes/wall.test.d.ts +2 -0
- package/dist/schema/nodes/wall.test.d.ts.map +1 -0
- package/dist/schema/nodes/wall.test.js +208 -0
- package/dist/schema/nodes/window.d.ts +11 -3
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/nodes/window.js +12 -0
- package/dist/schema/nodes/zone.d.ts +2 -0
- package/dist/schema/nodes/zone.d.ts.map +1 -1
- package/dist/schema/nodes/zone.js +6 -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/types.d.ts +2850 -655
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +49 -0
- package/dist/services/alignment-anchors.d.ts +100 -0
- package/dist/services/alignment-anchors.d.ts.map +1 -0
- package/dist/services/alignment-anchors.js +312 -0
- package/dist/services/alignment-anchors.test.d.ts +2 -0
- package/dist/services/alignment-anchors.test.d.ts.map +1 -0
- package/dist/services/alignment-anchors.test.js +383 -0
- package/dist/services/alignment.d.ts +131 -0
- package/dist/services/alignment.d.ts.map +1 -0
- package/dist/services/alignment.js +171 -0
- package/dist/services/alignment.test.d.ts +2 -0
- package/dist/services/alignment.test.d.ts.map +1 -0
- package/dist/services/alignment.test.js +92 -0
- package/dist/services/drag-session.d.ts +47 -0
- package/dist/services/drag-session.d.ts.map +1 -0
- package/dist/services/drag-session.js +100 -0
- package/dist/services/drag-session.test.d.ts +2 -0
- package/dist/services/drag-session.test.d.ts.map +1 -0
- package/dist/services/drag-session.test.js +219 -0
- package/dist/services/hosting.d.ts +88 -0
- package/dist/services/hosting.d.ts.map +1 -0
- package/dist/services/hosting.js +131 -0
- package/dist/services/hosting.test.d.ts +2 -0
- package/dist/services/hosting.test.d.ts.map +1 -0
- package/dist/services/hosting.test.js +230 -0
- package/dist/services/index.d.ts +13 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +12 -0
- package/dist/services/level-height.d.ts +28 -0
- package/dist/services/level-height.d.ts.map +1 -0
- package/dist/services/level-height.js +62 -0
- package/dist/services/movement.d.ts +48 -0
- package/dist/services/movement.d.ts.map +1 -0
- package/dist/services/movement.js +65 -0
- package/dist/services/movement.test.d.ts +2 -0
- package/dist/services/movement.test.d.ts.map +1 -0
- package/dist/services/movement.test.js +133 -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/opening-guides.test.d.ts +2 -0
- package/dist/services/opening-guides.test.d.ts.map +1 -0
- package/dist/services/opening-guides.test.js +208 -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/port-connectivity.test.d.ts +2 -0
- package/dist/services/port-connectivity.test.d.ts.map +1 -0
- package/dist/services/port-connectivity.test.js +297 -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/riser-diagram.test.d.ts +2 -0
- package/dist/services/riser-diagram.test.d.ts.map +1 -0
- package/dist/services/riser-diagram.test.js +66 -0
- package/dist/services/single-undo-dance.test.d.ts +2 -0
- package/dist/services/single-undo-dance.test.d.ts.map +1 -0
- package/dist/services/single-undo-dance.test.js +210 -0
- package/dist/services/snap.d.ts +80 -0
- package/dist/services/snap.d.ts.map +1 -0
- package/dist/services/snap.js +140 -0
- package/dist/services/snap.test.d.ts +2 -0
- package/dist/services/snap.test.d.ts.map +1 -0
- package/dist/services/snap.test.js +137 -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/system-graph.test.d.ts +2 -0
- package/dist/services/system-graph.test.d.ts.map +1 -0
- package/dist/services/system-graph.test.js +138 -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/services/validate-dwv.test.d.ts +2 -0
- package/dist/services/validate-dwv.test.d.ts.map +1 -0
- package/dist/services/validate-dwv.test.js +109 -0
- package/dist/solar-panel-presets.d.ts +17 -0
- package/dist/solar-panel-presets.d.ts.map +1 -0
- package/dist/solar-panel-presets.js +43 -0
- package/dist/store/actions/node-actions.d.ts +14 -2
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +585 -15
- package/dist/store/actions/node-mutation-sanitize.test.d.ts +2 -0
- package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +1 -0
- package/dist/store/actions/node-mutation-sanitize.test.js +147 -0
- package/dist/store/actions/reparent.test.d.ts +2 -0
- package/dist/store/actions/reparent.test.d.ts.map +1 -0
- package/dist/store/actions/reparent.test.js +212 -0
- package/dist/store/actions/ridge-vent-update.test.d.ts +2 -0
- package/dist/store/actions/ridge-vent-update.test.d.ts.map +1 -0
- package/dist/store/actions/ridge-vent-update.test.js +264 -0
- package/dist/store/history-control.d.ts +11 -0
- package/dist/store/history-control.d.ts.map +1 -1
- package/dist/store/history-control.js +33 -0
- package/dist/store/use-interactive.d.ts +39 -0
- package/dist/store/use-interactive.d.ts.map +1 -1
- package/dist/store/use-interactive.js +75 -0
- package/dist/store/use-live-node-overrides.d.ts +22 -0
- package/dist/store/use-live-node-overrides.d.ts.map +1 -0
- package/dist/store/use-live-node-overrides.js +59 -0
- package/dist/store/use-scene-dirty-tracking.test.d.ts +2 -0
- package/dist/store/use-scene-dirty-tracking.test.d.ts.map +1 -0
- package/dist/store/use-scene-dirty-tracking.test.js +68 -0
- package/dist/store/use-scene-elevator-migration.test.d.ts +2 -0
- package/dist/store/use-scene-elevator-migration.test.d.ts.map +1 -0
- package/dist/store/use-scene-elevator-migration.test.js +109 -0
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts +2 -0
- package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +1 -0
- package/dist/store/use-scene-measurement-roundtrip.test.js +97 -0
- package/dist/store/use-scene-plugins.test.d.ts +2 -0
- package/dist/store/use-scene-plugins.test.d.ts.map +1 -0
- package/dist/store/use-scene-plugins.test.js +51 -0
- package/dist/store/use-scene-wall-slot-migration.test.d.ts +2 -0
- package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +1 -0
- package/dist/store/use-scene-wall-slot-migration.test.js +210 -0
- package/dist/store/use-scene-window-migration.test.d.ts +2 -0
- package/dist/store/use-scene-window-migration.test.d.ts.map +1 -0
- package/dist/store/use-scene-window-migration.test.js +85 -0
- package/dist/store/use-scene.d.ts +28 -2
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +546 -49
- package/dist/systems/elevator/elevator-dispatch.d.ts +12 -0
- package/dist/systems/elevator/elevator-dispatch.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-dispatch.js +57 -0
- package/dist/systems/elevator/elevator-geometry.d.ts +15 -0
- package/dist/systems/elevator/elevator-geometry.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-geometry.js +50 -0
- package/dist/systems/elevator/elevator-opening-sync.d.ts +6 -0
- package/dist/systems/elevator/elevator-opening-sync.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-opening-sync.js +218 -0
- package/dist/systems/elevator/elevator-opening-sync.test.d.ts +2 -0
- package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-opening-sync.test.js +146 -0
- package/dist/systems/elevator/elevator-opening-system.d.ts +2 -0
- package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-opening-system.js +48 -0
- package/dist/systems/elevator/elevator-runtime-system.d.ts +2 -0
- package/dist/systems/elevator/elevator-runtime-system.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-runtime-system.js +8 -0
- package/dist/systems/elevator/elevator-runtime.d.ts +18 -0
- package/dist/systems/elevator/elevator-runtime.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-runtime.js +225 -0
- package/dist/systems/elevator/elevator-runtime.test.d.ts +2 -0
- package/dist/systems/elevator/elevator-runtime.test.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-runtime.test.js +64 -0
- package/dist/systems/elevator/elevator-service.d.ts +19 -0
- package/dist/systems/elevator/elevator-service.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-service.js +111 -0
- 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/fence/fence-spline.test.d.ts +2 -0
- package/dist/systems/fence/fence-spline.test.d.ts.map +1 -0
- package/dist/systems/fence/fence-spline.test.js +136 -0
- package/dist/systems/stair/stair-footprint.d.ts +44 -0
- package/dist/systems/stair/stair-footprint.d.ts.map +1 -0
- package/dist/systems/stair/stair-footprint.js +178 -0
- package/dist/systems/stair/stair-opening-preview.d.ts +2447 -0
- package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -0
- package/dist/systems/stair/stair-opening-preview.js +93 -0
- package/dist/systems/stair/stair-opening-preview.test.d.ts +2 -0
- package/dist/systems/stair/stair-opening-preview.test.d.ts.map +1 -0
- package/dist/systems/stair/stair-opening-preview.test.js +80 -0
- package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.js +155 -147
- package/dist/systems/stair/stair-opening-sync.test.js +424 -2
- package/dist/systems/stair/stair-opening-system.d.ts +2 -0
- package/dist/systems/stair/stair-opening-system.d.ts.map +1 -0
- package/dist/systems/stair/stair-opening-system.js +104 -0
- package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
- package/dist/systems/wall/wall-mitering.js +38 -2
- package/dist/systems/wall/wall-mitering.test.d.ts +2 -0
- package/dist/systems/wall/wall-mitering.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-mitering.test.js +63 -0
- package/dist/systems/wall/wall-move.d.ts +48 -0
- package/dist/systems/wall/wall-move.d.ts.map +1 -0
- package/dist/systems/wall/wall-move.js +193 -0
- package/dist/utils/clone-scene-graph.d.ts +7 -4
- package/dist/utils/clone-scene-graph.d.ts.map +1 -1
- package/dist/utils/clone-scene-graph.js +29 -7
- package/dist/utils/clone-scene-graph.test.d.ts +2 -0
- package/dist/utils/clone-scene-graph.test.d.ts.map +1 -0
- package/dist/utils/clone-scene-graph.test.js +62 -0
- 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 +90 -0
- package/dist/utils/heal-scene-graph.test.d.ts +2 -0
- package/dist/utils/heal-scene-graph.test.d.ts.map +1 -0
- package/dist/utils/heal-scene-graph.test.js +88 -0
- package/dist/validation/validate-build-json.d.ts +44 -0
- package/dist/validation/validate-build-json.d.ts.map +1 -0
- package/dist/validation/validate-build-json.js +256 -0
- package/package.json +11 -4
|
@@ -1,12 +1,24 @@
|
|
|
1
|
-
import { CeilingNode, SlabNode, } from '../schema';
|
|
1
|
+
import { CeilingNode, SlabNode, ZoneNode, } from '../schema';
|
|
2
2
|
import { getSceneHistoryPauseDepth, pauseSceneHistory, resumeSceneHistory, } from '../store/history-control';
|
|
3
3
|
import { getClampedWallCurveOffset, getWallCurveFrameAt, isCurvedWall, } from '../systems/wall/wall-curve';
|
|
4
4
|
import { simplifyClosedPolygon } from './polygon-geometry';
|
|
5
5
|
const DEFAULT_AUTO_SLAB_ELEVATION = 0.05;
|
|
6
6
|
const DEFAULT_AUTO_CEILING_HEIGHT = 2.5;
|
|
7
|
+
const CEILING_HEIGHT_EPSILON = 1e-6;
|
|
7
8
|
const ROOM_CURVE_TOLERANCE = 0.04;
|
|
8
9
|
const MAX_CURVE_SUBDIVISION_DEPTH = 6;
|
|
9
10
|
const AUTO_SLAB_POLYGON_SIMPLIFY_TOLERANCE = 0.08;
|
|
11
|
+
const WALL_ROOM_BOUNDARY_TOLERANCE = 0.08;
|
|
12
|
+
// A wall endpoint within this distance of another wall's interior is treated as a
|
|
13
|
+
// T-junction and splits that wall (see `splitStraightWallAtVertices`).
|
|
14
|
+
const WALL_JUNCTION_TOLERANCE = 0.08;
|
|
15
|
+
// An unmatched auto slab/ceiling whose polygon is still substantially covered
|
|
16
|
+
// by a detected room was absorbed by a room merge — the surviving auto surface
|
|
17
|
+
// owns that area, so keeping it would z-fight and it is deleted. Below this
|
|
18
|
+
// coverage the room genuinely ceased to exist (e.g. an enclosing wall was
|
|
19
|
+
// deleted) and the node is demoted to manual so user data survives.
|
|
20
|
+
const ORPHAN_MERGE_COVERAGE_THRESHOLD = 0.6;
|
|
21
|
+
const COVERAGE_SAMPLE_STEPS = 12;
|
|
10
22
|
function pointFromTuple(point) {
|
|
11
23
|
return { x: point[0], y: point[1] };
|
|
12
24
|
}
|
|
@@ -114,6 +126,120 @@ function bboxOverlapArea(a, b) {
|
|
|
114
126
|
const iy = Math.max(0, Math.min(a.maxY, b.maxY) - Math.max(a.minY, b.minY));
|
|
115
127
|
return ix * iy;
|
|
116
128
|
}
|
|
129
|
+
// Fraction of `subject`'s area lying inside any of `covers`, estimated by
|
|
130
|
+
// sampling a grid of cell centers over the subject's bbox. Cheap and robust
|
|
131
|
+
// enough for the merge-vs-demote decision; exact polygon clipping would be a
|
|
132
|
+
// heavy dependency for a 60% threshold.
|
|
133
|
+
function polygonCoverageRatio(subject, covers) {
|
|
134
|
+
if (subject.length < 3 || covers.length === 0)
|
|
135
|
+
return 0;
|
|
136
|
+
const bbox = bboxOf(subject);
|
|
137
|
+
const width = bbox.maxX - bbox.minX;
|
|
138
|
+
const height = bbox.maxY - bbox.minY;
|
|
139
|
+
let inside = 0;
|
|
140
|
+
let covered = 0;
|
|
141
|
+
for (let i = 0; i < COVERAGE_SAMPLE_STEPS; i += 1) {
|
|
142
|
+
for (let j = 0; j < COVERAGE_SAMPLE_STEPS; j += 1) {
|
|
143
|
+
const point = {
|
|
144
|
+
x: bbox.minX + ((i + 0.5) / COVERAGE_SAMPLE_STEPS) * width,
|
|
145
|
+
y: bbox.minY + ((j + 0.5) / COVERAGE_SAMPLE_STEPS) * height,
|
|
146
|
+
};
|
|
147
|
+
if (!pointInPolygon(point, subject))
|
|
148
|
+
continue;
|
|
149
|
+
inside += 1;
|
|
150
|
+
if (pointInAnyPolygon(point, covers))
|
|
151
|
+
covered += 1;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (inside === 0) {
|
|
155
|
+
return pointInAnyPolygon(polygonCentroid(subject), covers) ? 1 : 0;
|
|
156
|
+
}
|
|
157
|
+
return covered / inside;
|
|
158
|
+
}
|
|
159
|
+
// Demoted auto surfaces keep their polygon untouched, so a re-closed room
|
|
160
|
+
// usually hits the exact-signature manual check. Coverage handles the rest:
|
|
161
|
+
// a room split across multiple manual surfaces AND a single manual surface
|
|
162
|
+
// spanning multiple rooms both suppress a replacement auto surface — what
|
|
163
|
+
// matters is that the ROOM is already substantially covered, not that any
|
|
164
|
+
// one manual surface belongs to it (a per-surface "mostly inside the room"
|
|
165
|
+
// filter dropped multi-room slabs and resurrected deleted auto slabs).
|
|
166
|
+
function matchesManualFootprint(roomPolygon, manualPolygons) {
|
|
167
|
+
return polygonCoverageRatio(roomPolygon, manualPolygons) >= ORPHAN_MERGE_COVERAGE_THRESHOLD;
|
|
168
|
+
}
|
|
169
|
+
function pointDistanceToPolygonBoundary(point, polygon) {
|
|
170
|
+
let minDistance = Number.POSITIVE_INFINITY;
|
|
171
|
+
for (let index = 0; index < polygon.length; index += 1) {
|
|
172
|
+
const start = polygon[index];
|
|
173
|
+
const end = polygon[(index + 1) % polygon.length];
|
|
174
|
+
if (!(start && end))
|
|
175
|
+
continue;
|
|
176
|
+
minDistance = Math.min(minDistance, distanceToSegment(pointToTuple(point), pointToTuple(start), pointToTuple(end)));
|
|
177
|
+
}
|
|
178
|
+
return minDistance;
|
|
179
|
+
}
|
|
180
|
+
function wallBoundsRoom(wall, roomPolygon) {
|
|
181
|
+
const sampled = sampleWallPointsForRoomDetection(wall);
|
|
182
|
+
if (sampled.length === 0)
|
|
183
|
+
return false;
|
|
184
|
+
const candidates = sampled.length === 2
|
|
185
|
+
? [
|
|
186
|
+
sampled[0],
|
|
187
|
+
{
|
|
188
|
+
x: (sampled[0].x + sampled[1].x) / 2,
|
|
189
|
+
y: (sampled[0].y + sampled[1].y) / 2,
|
|
190
|
+
},
|
|
191
|
+
sampled[1],
|
|
192
|
+
]
|
|
193
|
+
: sampled;
|
|
194
|
+
const matchingPoints = candidates.filter((point) => pointDistanceToPolygonBoundary(point, roomPolygon) <= WALL_ROOM_BOUNDARY_TOLERANCE);
|
|
195
|
+
return matchingPoints.length >= 2;
|
|
196
|
+
}
|
|
197
|
+
function pointIsOnSlab(point, slab) {
|
|
198
|
+
if (slab.polygon.length < 3)
|
|
199
|
+
return false;
|
|
200
|
+
const slabPolygon = slab.polygon.map(pointFromTuple);
|
|
201
|
+
if (!pointInPolygon(point, slabPolygon))
|
|
202
|
+
return false;
|
|
203
|
+
for (const hole of slab.holes ?? []) {
|
|
204
|
+
if (hole.length >= 3 && pointInPolygon(point, hole.map(pointFromTuple))) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
function slabSupportsRoom(roomPolygon, slab) {
|
|
211
|
+
if (slab.polygon.length < 3)
|
|
212
|
+
return false;
|
|
213
|
+
if (polygonSignature(slab.polygon.map(pointFromTuple)) === polygonSignature(roomPolygon)) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
return pointIsOnSlab(polygonCentroid(roomPolygon), slab);
|
|
217
|
+
}
|
|
218
|
+
function resolveRoomSlabElevation(roomPolygon, slabs = []) {
|
|
219
|
+
let maxElevation = 0;
|
|
220
|
+
for (const slab of slabs) {
|
|
221
|
+
if (!slabSupportsRoom(roomPolygon, slab))
|
|
222
|
+
continue;
|
|
223
|
+
maxElevation = Math.max(maxElevation, slab.elevation ?? DEFAULT_AUTO_SLAB_ELEVATION);
|
|
224
|
+
}
|
|
225
|
+
return maxElevation;
|
|
226
|
+
}
|
|
227
|
+
function resolveRoomWallHeight(roomPolygon, walls = []) {
|
|
228
|
+
let maxHeight = 0;
|
|
229
|
+
for (const wall of walls) {
|
|
230
|
+
if (!wallBoundsRoom(wall, roomPolygon))
|
|
231
|
+
continue;
|
|
232
|
+
const height = wall.height ?? DEFAULT_AUTO_CEILING_HEIGHT;
|
|
233
|
+
if (Number.isFinite(height)) {
|
|
234
|
+
maxHeight = Math.max(maxHeight, height);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return maxHeight > 0 ? maxHeight : DEFAULT_AUTO_CEILING_HEIGHT;
|
|
238
|
+
}
|
|
239
|
+
function resolveAutoCeilingHeight(roomPolygon, context = {}) {
|
|
240
|
+
return (resolveRoomSlabElevation(roomPolygon, context.slabs) +
|
|
241
|
+
resolveRoomWallHeight(roomPolygon, context.walls));
|
|
242
|
+
}
|
|
117
243
|
function getWallDirection(wall) {
|
|
118
244
|
const dx = wall.end[0] - wall.start[0];
|
|
119
245
|
const dy = wall.end[1] - wall.start[1];
|
|
@@ -164,11 +290,52 @@ function sampleWallPointsForRoomDetection(wall, tolerance = ROOM_CURVE_TOLERANCE
|
|
|
164
290
|
};
|
|
165
291
|
return subdivide(0, start, 1, end, 0);
|
|
166
292
|
}
|
|
167
|
-
function
|
|
168
|
-
const
|
|
169
|
-
|
|
293
|
+
function segmentProjection(point, start, end) {
|
|
294
|
+
const dx = end.x - start.x;
|
|
295
|
+
const dy = end.y - start.y;
|
|
296
|
+
const lengthSquared = dx * dx + dy * dy;
|
|
297
|
+
if (lengthSquared < 1e-12) {
|
|
298
|
+
return { t: 0, distance: Math.hypot(point.x - start.x, point.y - start.y) };
|
|
299
|
+
}
|
|
300
|
+
const t = ((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSquared;
|
|
301
|
+
const clampedT = Math.max(0, Math.min(1, t));
|
|
302
|
+
const projX = start.x + clampedT * dx;
|
|
303
|
+
const projY = start.y + clampedT * dy;
|
|
304
|
+
return { t, distance: Math.hypot(point.x - projX, point.y - projY) };
|
|
170
305
|
}
|
|
171
|
-
|
|
306
|
+
// Break a straight wall at any junction vertex (another wall's endpoint) that
|
|
307
|
+
// lands on its interior, returning the ordered polyline [start, …splits, end].
|
|
308
|
+
// Splitting at the *vertex* position (not the projection) keeps the split node's
|
|
309
|
+
// key identical to the touching wall's endpoint so the two share a graph node.
|
|
310
|
+
function splitStraightWallAtVertices(start, end, vertices) {
|
|
311
|
+
const length = Math.hypot(end.x - start.x, end.y - start.y);
|
|
312
|
+
if (length < 1e-9)
|
|
313
|
+
return [start, end];
|
|
314
|
+
const interior = [];
|
|
315
|
+
for (const vertex of vertices) {
|
|
316
|
+
const { t, distance } = segmentProjection(vertex, start, end);
|
|
317
|
+
if (distance > WALL_JUNCTION_TOLERANCE)
|
|
318
|
+
continue;
|
|
319
|
+
const along = t * length;
|
|
320
|
+
if (along <= WALL_JUNCTION_TOLERANCE || along >= length - WALL_JUNCTION_TOLERANCE)
|
|
321
|
+
continue;
|
|
322
|
+
interior.push({ point: vertex, t });
|
|
323
|
+
}
|
|
324
|
+
interior.sort((a, b) => a.t - b.t);
|
|
325
|
+
const ordered = [start];
|
|
326
|
+
let lastKey = pointKey(start);
|
|
327
|
+
for (const { point } of interior) {
|
|
328
|
+
const key = pointKey(point);
|
|
329
|
+
if (key === lastKey)
|
|
330
|
+
continue;
|
|
331
|
+
ordered.push(point);
|
|
332
|
+
lastKey = key;
|
|
333
|
+
}
|
|
334
|
+
if (lastKey !== pointKey(end))
|
|
335
|
+
ordered.push(end);
|
|
336
|
+
return ordered;
|
|
337
|
+
}
|
|
338
|
+
function extractRooms(walls) {
|
|
172
339
|
if (walls.length < 3)
|
|
173
340
|
return [];
|
|
174
341
|
const graph = new Map();
|
|
@@ -180,35 +347,71 @@ function extractRoomPolygons(walls) {
|
|
|
180
347
|
}
|
|
181
348
|
return key;
|
|
182
349
|
};
|
|
350
|
+
// Planarize first: collect every wall endpoint as a candidate graph vertex so
|
|
351
|
+
// straight walls can be split at T-junctions where another wall ends mid-span.
|
|
352
|
+
// Without this the touching wall's endpoint is a dangling degree-1 node and the
|
|
353
|
+
// enclosed area (e.g. a room added against the middle of an existing wall)
|
|
354
|
+
// never forms a cycle.
|
|
355
|
+
const vertexByKey = new Map();
|
|
356
|
+
for (const wall of walls) {
|
|
357
|
+
for (const tuple of [wall.start, wall.end]) {
|
|
358
|
+
const point = pointFromTuple(tuple);
|
|
359
|
+
const key = pointKey(point);
|
|
360
|
+
if (!vertexByKey.has(key))
|
|
361
|
+
vertexByKey.set(key, point);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const vertices = [...vertexByKey.values()];
|
|
183
365
|
for (const wall of walls) {
|
|
184
366
|
const start = pointFromTuple(wall.start);
|
|
185
367
|
const end = pointFromTuple(wall.end);
|
|
186
|
-
|
|
187
|
-
const endKey = upsertNode(end);
|
|
188
|
-
if (startKey === endKey)
|
|
368
|
+
if (samePointWithinTolerance(start, end))
|
|
189
369
|
continue;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
fromKey
|
|
206
|
-
toKey
|
|
207
|
-
|
|
208
|
-
|
|
370
|
+
// Curved walls keep their sampled polyline as one edge; straight walls split
|
|
371
|
+
// into consecutive sub-edges at their interior junction vertices.
|
|
372
|
+
const subPolylines = isCurvedWall(wall)
|
|
373
|
+
? [sampleWallPointsForRoomDetection(wall)]
|
|
374
|
+
: (() => {
|
|
375
|
+
const ordered = splitStraightWallAtVertices(start, end, vertices);
|
|
376
|
+
const parts = [];
|
|
377
|
+
for (let index = 0; index < ordered.length - 1; index += 1) {
|
|
378
|
+
parts.push([ordered[index], ordered[index + 1]]);
|
|
379
|
+
}
|
|
380
|
+
return parts;
|
|
381
|
+
})();
|
|
382
|
+
subPolylines.forEach((points, subIndex) => {
|
|
383
|
+
const from = points[0];
|
|
384
|
+
const to = points[points.length - 1];
|
|
385
|
+
const fromKey = upsertNode(from);
|
|
386
|
+
const toKey = upsertNode(to);
|
|
387
|
+
if (fromKey === toKey)
|
|
388
|
+
return;
|
|
389
|
+
const reversePoints = [...points].reverse();
|
|
390
|
+
const forwardId = `${wall.id}#${subIndex}:f`;
|
|
391
|
+
const reverseId = `${wall.id}#${subIndex}:r`;
|
|
392
|
+
halfEdges.set(forwardId, {
|
|
393
|
+
id: forwardId,
|
|
394
|
+
reverseId,
|
|
395
|
+
fromKey,
|
|
396
|
+
toKey,
|
|
397
|
+
angle: Math.atan2(points[1].y - from.y, points[1].x - from.x),
|
|
398
|
+
points,
|
|
399
|
+
wallId: wall.id,
|
|
400
|
+
face: 'front',
|
|
401
|
+
});
|
|
402
|
+
halfEdges.set(reverseId, {
|
|
403
|
+
id: reverseId,
|
|
404
|
+
reverseId: forwardId,
|
|
405
|
+
fromKey: toKey,
|
|
406
|
+
toKey: fromKey,
|
|
407
|
+
angle: Math.atan2(reversePoints[1].y - to.y, reversePoints[1].x - to.x),
|
|
408
|
+
points: reversePoints,
|
|
409
|
+
wallId: wall.id,
|
|
410
|
+
face: 'back',
|
|
411
|
+
});
|
|
412
|
+
graph.get(fromKey)?.outgoing.push(forwardId);
|
|
413
|
+
graph.get(toKey)?.outgoing.push(reverseId);
|
|
209
414
|
});
|
|
210
|
-
graph.get(startKey)?.outgoing.push(forwardId);
|
|
211
|
-
graph.get(endKey)?.outgoing.push(reverseId);
|
|
212
415
|
}
|
|
213
416
|
const sortedOutgoing = new Map();
|
|
214
417
|
for (const [key, node] of graph.entries()) {
|
|
@@ -229,15 +432,49 @@ function extractRoomPolygons(walls) {
|
|
|
229
432
|
const nextIdx = (idx - 1 + outgoing.length) % outgoing.length;
|
|
230
433
|
return outgoing[nextIdx] ?? null;
|
|
231
434
|
};
|
|
435
|
+
const splitIntoSimpleCycles = (walkEdgeIds) => {
|
|
436
|
+
const cycles = [];
|
|
437
|
+
const firstEdge = halfEdges.get(walkEdgeIds[0] ?? '');
|
|
438
|
+
if (!firstEdge)
|
|
439
|
+
return cycles;
|
|
440
|
+
const pathEdges = [];
|
|
441
|
+
const pathVertices = [firstEdge.fromKey];
|
|
442
|
+
const vertexIndex = new Map([[firstEdge.fromKey, 0]]);
|
|
443
|
+
for (const edgeId of walkEdgeIds) {
|
|
444
|
+
const edge = halfEdges.get(edgeId);
|
|
445
|
+
if (!edge || edge.fromKey !== pathVertices[pathVertices.length - 1])
|
|
446
|
+
return [];
|
|
447
|
+
pathEdges.push(edgeId);
|
|
448
|
+
const repeatedIndex = vertexIndex.get(edge.toKey);
|
|
449
|
+
if (repeatedIndex === undefined) {
|
|
450
|
+
pathVertices.push(edge.toKey);
|
|
451
|
+
vertexIndex.set(edge.toKey, pathVertices.length - 1);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const cycle = pathEdges.slice(repeatedIndex);
|
|
455
|
+
if (cycle.length >= 3)
|
|
456
|
+
cycles.push(cycle);
|
|
457
|
+
for (let index = repeatedIndex + 1; index < pathVertices.length; index += 1) {
|
|
458
|
+
vertexIndex.delete(pathVertices[index]);
|
|
459
|
+
}
|
|
460
|
+
pathVertices.length = repeatedIndex + 1;
|
|
461
|
+
pathEdges.length = repeatedIndex;
|
|
462
|
+
}
|
|
463
|
+
return pathEdges.length === 0 && pathVertices.length === 1 ? cycles : [];
|
|
464
|
+
};
|
|
232
465
|
const visitedDirected = new Set();
|
|
233
|
-
const
|
|
234
|
-
|
|
466
|
+
const rooms = [];
|
|
467
|
+
// A face walk cannot revisit a half-edge, so the half-edge count bounds its
|
|
468
|
+
// length. It can revisit a vertex when dangling walls or other graph bridges
|
|
469
|
+
// are traced out and back; those excursions are removed below.
|
|
470
|
+
const maxSteps = Math.min(2000, halfEdges.size + 10);
|
|
235
471
|
for (const edgeId of halfEdges.keys()) {
|
|
236
472
|
if (visitedDirected.has(edgeId))
|
|
237
473
|
continue;
|
|
238
474
|
const cycleEdgeIds = [];
|
|
239
475
|
let currentEdgeId = edgeId;
|
|
240
476
|
let valid = true;
|
|
477
|
+
let closed = false;
|
|
241
478
|
for (let step = 0; step < maxSteps; step += 1) {
|
|
242
479
|
const currentEdge = halfEdges.get(currentEdgeId);
|
|
243
480
|
if (!currentEdge) {
|
|
@@ -252,29 +489,64 @@ function extractRoomPolygons(walls) {
|
|
|
252
489
|
break;
|
|
253
490
|
}
|
|
254
491
|
currentEdgeId = next;
|
|
255
|
-
if (currentEdgeId === edgeId)
|
|
492
|
+
if (currentEdgeId === edgeId) {
|
|
493
|
+
closed = true;
|
|
256
494
|
break;
|
|
495
|
+
}
|
|
257
496
|
}
|
|
258
|
-
if (!valid || cycleEdgeIds.length < 3)
|
|
259
|
-
continue;
|
|
260
|
-
const polygon = dedupeSequentialPoints(cycleEdgeIds.flatMap((id, index) => {
|
|
261
|
-
const points = halfEdges.get(id)?.points ?? [];
|
|
262
|
-
return index === cycleEdgeIds.length - 1 ? points : points.slice(0, -1);
|
|
263
|
-
}));
|
|
264
|
-
if (polygon.length < 3)
|
|
497
|
+
if (!(valid && closed) || cycleEdgeIds.length < 3)
|
|
265
498
|
continue;
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
499
|
+
for (const simpleCycleEdgeIds of splitIntoSimpleCycles(cycleEdgeIds)) {
|
|
500
|
+
const polygon = dedupeSequentialPoints(simpleCycleEdgeIds.flatMap((id, index) => {
|
|
501
|
+
const points = halfEdges.get(id)?.points ?? [];
|
|
502
|
+
return index === simpleCycleEdgeIds.length - 1 ? points : points.slice(0, -1);
|
|
503
|
+
}));
|
|
504
|
+
if (polygon.length < 3)
|
|
505
|
+
continue;
|
|
506
|
+
const signedArea = polygonArea(polygon);
|
|
507
|
+
if (signedArea <= 0)
|
|
508
|
+
continue;
|
|
509
|
+
if (signedArea < 0.5 || signedArea > 10_000)
|
|
510
|
+
continue;
|
|
511
|
+
const signature = polygonSignature(polygon);
|
|
512
|
+
if (rooms.some((room) => polygonSignature(room.polygon) === signature))
|
|
513
|
+
continue;
|
|
514
|
+
rooms.push({
|
|
515
|
+
polygon,
|
|
516
|
+
boundaryFaces: simpleCycleEdgeIds.flatMap((id) => {
|
|
517
|
+
const edge = halfEdges.get(id);
|
|
518
|
+
if (!edge)
|
|
519
|
+
return [];
|
|
520
|
+
return [
|
|
521
|
+
{
|
|
522
|
+
wallId: edge.wallId,
|
|
523
|
+
face: edge.face,
|
|
524
|
+
points: edge.points.map(pointToTuple),
|
|
525
|
+
},
|
|
526
|
+
];
|
|
527
|
+
}),
|
|
528
|
+
});
|
|
529
|
+
}
|
|
275
530
|
}
|
|
276
|
-
|
|
277
|
-
return
|
|
531
|
+
rooms.sort((a, b) => Math.abs(polygonArea(b.polygon)) - Math.abs(polygonArea(a.polygon)));
|
|
532
|
+
return rooms;
|
|
533
|
+
}
|
|
534
|
+
function extractRoomPolygons(walls) {
|
|
535
|
+
return extractRooms(walls).map((room) => room.polygon);
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* True when `wall` lies on the boundary of a room enclosed by `walls`, using the
|
|
539
|
+
* same planar room graph the auto slab/ceiling sync uses. The wall builder's
|
|
540
|
+
* "Room (auto-close)" mode calls this so drafting stops the moment a segment
|
|
541
|
+
* closes a room — whether the chain loops back to its own start or seals a bay
|
|
542
|
+
* against the middle of an existing wall (a T-junction). Sharing one graph means
|
|
543
|
+
* auto-close and auto-slab detection can never disagree about what is "closed".
|
|
544
|
+
*/
|
|
545
|
+
export function wallClosesRoom(walls, wall) {
|
|
546
|
+
const roomPolygons = extractRoomPolygons(walls);
|
|
547
|
+
if (roomPolygons.length === 0)
|
|
548
|
+
return false;
|
|
549
|
+
return roomPolygons.some((polygon) => wallBoundsRoom(wall, polygon));
|
|
278
550
|
}
|
|
279
551
|
export function resolveWallSurfaceSides(wall, roomPolygons) {
|
|
280
552
|
if (roomPolygons.length === 0) {
|
|
@@ -340,26 +612,111 @@ function wallGeometrySignature(wall) {
|
|
|
340
612
|
wall.end[0].toFixed(4),
|
|
341
613
|
wall.end[1].toFixed(4),
|
|
342
614
|
(wall.thickness ?? 0.2).toFixed(4),
|
|
615
|
+
(wall.height ?? DEFAULT_AUTO_CEILING_HEIGHT).toFixed(4),
|
|
343
616
|
getClampedWallCurveOffset(wall).toFixed(4),
|
|
344
617
|
].join('|');
|
|
345
618
|
}
|
|
346
619
|
function levelWallSnapshot(walls) {
|
|
347
620
|
return walls.map(wallGeometrySignature).sort().join('||');
|
|
348
621
|
}
|
|
349
|
-
function
|
|
350
|
-
|
|
622
|
+
function zoneGeometrySignature(zone) {
|
|
623
|
+
return [
|
|
624
|
+
zone.id,
|
|
625
|
+
zone.autoFromWalls ? 'auto' : 'manual',
|
|
626
|
+
zone.boundaryWallIds.slice().sort().join(','),
|
|
627
|
+
zone.polygon.map(([x, z]) => `${x.toFixed(4)},${z.toFixed(4)}`).join(';'),
|
|
628
|
+
].join('|');
|
|
629
|
+
}
|
|
630
|
+
// Slabs and ceilings stay out of the trigger signature: including generated
|
|
631
|
+
// surfaces caused delete/recreate feedback. Zones are included only so a newly
|
|
632
|
+
// traced room footprint can adopt its enclosing walls without waiting for the
|
|
633
|
+
// next remodel.
|
|
634
|
+
function levelStructureSnapshots(nodes) {
|
|
635
|
+
const wallsByLevel = new Map();
|
|
636
|
+
const zonesByLevel = new Map();
|
|
637
|
+
for (const node of Object.values(nodes)) {
|
|
638
|
+
if (!(node && typeof node === 'object' && 'parentId' in node && node.parentId))
|
|
639
|
+
continue;
|
|
640
|
+
const levelId = node.parentId;
|
|
641
|
+
if (node.type === 'wall') {
|
|
642
|
+
const walls = wallsByLevel.get(levelId) ?? [];
|
|
643
|
+
walls.push(node);
|
|
644
|
+
wallsByLevel.set(levelId, walls);
|
|
645
|
+
}
|
|
646
|
+
else if (node.type === 'zone') {
|
|
647
|
+
const zones = zonesByLevel.get(levelId) ?? [];
|
|
648
|
+
zones.push(ZoneNode.parse(node));
|
|
649
|
+
zonesByLevel.set(levelId, zones);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
const snapshots = new Map();
|
|
653
|
+
const levelIds = new Set([...wallsByLevel.keys(), ...zonesByLevel.keys()]);
|
|
654
|
+
for (const levelId of levelIds) {
|
|
655
|
+
const walls = wallsByLevel.get(levelId) ?? [];
|
|
656
|
+
const zones = zonesByLevel.get(levelId) ?? [];
|
|
657
|
+
snapshots.set(levelId, `${levelWallSnapshot(walls)}##${zones.map(zoneGeometrySignature).sort().join('||')}`);
|
|
658
|
+
}
|
|
659
|
+
return snapshots;
|
|
660
|
+
}
|
|
661
|
+
function buildSpace(levelId, room) {
|
|
662
|
+
const signature = polygonSignature(room.polygon);
|
|
351
663
|
return {
|
|
352
664
|
id: `space-${levelId}-${signature.slice(0, 12)}`,
|
|
353
665
|
levelId,
|
|
354
|
-
polygon: polygon.map(pointToTuple),
|
|
355
|
-
wallIds: [],
|
|
666
|
+
polygon: room.polygon.map(pointToTuple),
|
|
667
|
+
wallIds: [...new Set(room.boundaryFaces.map((boundary) => boundary.wallId))],
|
|
668
|
+
boundaryFaces: room.boundaryFaces,
|
|
356
669
|
isExterior: false,
|
|
357
670
|
};
|
|
358
671
|
}
|
|
359
|
-
function
|
|
672
|
+
function sameStringSet(a, b) {
|
|
673
|
+
if (a.length !== b.length)
|
|
674
|
+
return false;
|
|
675
|
+
const right = new Set(b);
|
|
676
|
+
return a.every((value) => right.has(value));
|
|
677
|
+
}
|
|
678
|
+
export function planAutoZonesForLevel(spaces, existingZones) {
|
|
679
|
+
const update = [];
|
|
680
|
+
for (const zone of existingZones) {
|
|
681
|
+
const storedSignature = polygonSignature(zone.polygon.map(pointFromTuple));
|
|
682
|
+
const matchingSpace = zone.autoFromWalls && zone.boundaryWallIds.length >= 3
|
|
683
|
+
? spaces.find((space) => sameStringSet(space.wallIds, zone.boundaryWallIds))
|
|
684
|
+
: spaces.find((space) => polygonSignature(space.polygon.map(pointFromTuple)) === storedSignature);
|
|
685
|
+
if (!matchingSpace)
|
|
686
|
+
continue;
|
|
687
|
+
const data = {};
|
|
688
|
+
if (!zone.autoFromWalls)
|
|
689
|
+
data.autoFromWalls = true;
|
|
690
|
+
if (!sameStringSet(zone.boundaryWallIds, matchingSpace.wallIds)) {
|
|
691
|
+
data.boundaryWallIds = matchingSpace.wallIds;
|
|
692
|
+
}
|
|
693
|
+
if (!sameTuplePolygon(zone.polygon, matchingSpace.polygon)) {
|
|
694
|
+
data.polygon = matchingSpace.polygon;
|
|
695
|
+
}
|
|
696
|
+
if (Object.keys(data).length > 0)
|
|
697
|
+
update.push({ id: zone.id, data });
|
|
698
|
+
}
|
|
699
|
+
return { update };
|
|
700
|
+
}
|
|
701
|
+
export function resolveAutoZonePolygon(zone, resolve) {
|
|
702
|
+
if (!zone.autoFromWalls || zone.boundaryWallIds.length < 3)
|
|
703
|
+
return zone.polygon;
|
|
704
|
+
const walls = zone.boundaryWallIds.flatMap((id) => {
|
|
705
|
+
const node = resolve(id);
|
|
706
|
+
return node && typeof node === 'object' && 'type' in node && node.type === 'wall'
|
|
707
|
+
? [node]
|
|
708
|
+
: [];
|
|
709
|
+
});
|
|
710
|
+
if (walls.length !== zone.boundaryWallIds.length)
|
|
711
|
+
return zone.polygon;
|
|
712
|
+
const room = extractRooms(walls).find((candidate) => sameStringSet([...new Set(candidate.boundaryFaces.map((boundary) => boundary.wallId))], zone.boundaryWallIds));
|
|
713
|
+
return room ? room.polygon.map(pointToTuple) : zone.polygon;
|
|
714
|
+
}
|
|
715
|
+
export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
|
|
360
716
|
const manualSlabs = existingSlabs.filter((slab) => !slab.autoFromWalls);
|
|
361
717
|
const manualSignatures = new Set(manualSlabs.map((slab) => polygonSignature(slab.polygon.map(pointFromTuple))));
|
|
362
|
-
const
|
|
718
|
+
const manualPolygons = manualSlabs.map((slab) => slab.polygon.map(pointFromTuple));
|
|
719
|
+
const detectedAll = roomPolygons
|
|
363
720
|
.map((poly) => ({
|
|
364
721
|
poly: simplifyClosedPolygon(poly.map(pointToTuple), AUTO_SLAB_POLYGON_SIMPLIFY_TOLERANCE).map(pointFromTuple),
|
|
365
722
|
sig: '',
|
|
@@ -373,8 +730,8 @@ function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore)
|
|
|
373
730
|
centroid: polygonCentroid(room.poly),
|
|
374
731
|
area: Math.abs(polygonArea(room.poly)),
|
|
375
732
|
bbox: bboxOf(room.poly),
|
|
376
|
-
}))
|
|
377
|
-
|
|
733
|
+
}));
|
|
734
|
+
const detected = detectedAll.filter(({ sig, poly }) => !manualSignatures.has(sig) && !matchesManualFootprint(poly, manualPolygons));
|
|
378
735
|
const existingAuto = existingSlabs.filter((slab) => slab.autoFromWalls);
|
|
379
736
|
const existingAutoMeta = existingAuto.map((slab) => {
|
|
380
737
|
const poly = slab.polygon.map(pointFromTuple);
|
|
@@ -391,10 +748,12 @@ function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore)
|
|
|
391
748
|
const updatesById = new Map();
|
|
392
749
|
const autoBySignature = new Map();
|
|
393
750
|
for (const entry of existingAutoMeta) {
|
|
394
|
-
autoBySignature.
|
|
751
|
+
const bucket = autoBySignature.get(entry.sig) ?? [];
|
|
752
|
+
bucket.push(entry);
|
|
753
|
+
autoBySignature.set(entry.sig, bucket);
|
|
395
754
|
}
|
|
396
755
|
detected.forEach((room, index) => {
|
|
397
|
-
const existing = autoBySignature.get(room.sig);
|
|
756
|
+
const existing = autoBySignature.get(room.sig)?.shift();
|
|
398
757
|
if (!existing)
|
|
399
758
|
return;
|
|
400
759
|
matchedDetectedIdx.add(index);
|
|
@@ -430,17 +789,33 @@ function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore)
|
|
|
430
789
|
matchedSlabIds.add(bestMatch.entry.slab.id);
|
|
431
790
|
updatesById.set(bestMatch.entry.slab.id, room.poly.map(pointToTuple));
|
|
432
791
|
}
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const
|
|
440
|
-
if (
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
792
|
+
const detectedRoomPolygons = detectedAll.map((room) => room.poly);
|
|
793
|
+
const slabsToDelete = [];
|
|
794
|
+
const slabDemotions = [];
|
|
795
|
+
for (const slab of existingAuto) {
|
|
796
|
+
if (updatesById.has(slab.id))
|
|
797
|
+
continue;
|
|
798
|
+
const coverage = polygonCoverageRatio(slab.polygon.map(pointFromTuple), detectedRoomPolygons);
|
|
799
|
+
if (coverage >= ORPHAN_MERGE_COVERAGE_THRESHOLD) {
|
|
800
|
+
slabsToDelete.push(slab.id);
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
// Render offsets derive from level context at geometry build time, so
|
|
804
|
+
// demotion leaves the stored polygon untouched (same as ceilings).
|
|
805
|
+
slabDemotions.push({ id: slab.id, data: { autoFromWalls: false } });
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
const slabsToUpdate = [
|
|
809
|
+
...existingAuto
|
|
810
|
+
.filter((slab) => updatesById.has(slab.id))
|
|
811
|
+
.flatMap((slab) => {
|
|
812
|
+
const polygon = updatesById.get(slab.id);
|
|
813
|
+
if (!polygon)
|
|
814
|
+
return [];
|
|
815
|
+
return sameTuplePolygon(slab.polygon, polygon) ? [] : [{ id: slab.id, data: { polygon } }];
|
|
816
|
+
}),
|
|
817
|
+
...slabDemotions,
|
|
818
|
+
];
|
|
444
819
|
const plannedSlabsForNaming = [...existingSlabs];
|
|
445
820
|
const slabsToCreate = [];
|
|
446
821
|
for (let index = 0; index < detected.length; index += 1) {
|
|
@@ -459,20 +834,46 @@ function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore)
|
|
|
459
834
|
autoFromWalls: true,
|
|
460
835
|
}));
|
|
461
836
|
}
|
|
462
|
-
|
|
463
|
-
|
|
837
|
+
return {
|
|
838
|
+
create: slabsToCreate,
|
|
839
|
+
update: slabsToUpdate,
|
|
840
|
+
delete: slabsToDelete,
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore) {
|
|
844
|
+
const plan = planAutoSlabsForLevel(roomPolygons, existingSlabs);
|
|
845
|
+
if (plan.delete.length > 0) {
|
|
846
|
+
sceneStore.getState().deleteNodes(plan.delete);
|
|
464
847
|
}
|
|
465
|
-
if (
|
|
466
|
-
sceneStore.getState().updateNodes(
|
|
848
|
+
if (plan.update.length > 0) {
|
|
849
|
+
sceneStore.getState().updateNodes(plan.update);
|
|
467
850
|
}
|
|
468
|
-
if (
|
|
469
|
-
sceneStore.getState().createNodes(
|
|
851
|
+
if (plan.create.length > 0) {
|
|
852
|
+
sceneStore.getState().createNodes(plan.create.map((node) => ({ node, parentId: levelId })));
|
|
470
853
|
}
|
|
854
|
+
return plan;
|
|
471
855
|
}
|
|
472
|
-
function
|
|
856
|
+
export function projectAutoSlabsForPlan(existingSlabs, plan) {
|
|
857
|
+
const slabsById = new Map(existingSlabs.map((slab) => [slab.id, slab]));
|
|
858
|
+
for (const id of plan.delete) {
|
|
859
|
+
slabsById.delete(id);
|
|
860
|
+
}
|
|
861
|
+
for (const update of plan.update) {
|
|
862
|
+
const slab = slabsById.get(update.id);
|
|
863
|
+
if (!slab)
|
|
864
|
+
continue;
|
|
865
|
+
slabsById.set(update.id, SlabNode.parse({ ...slab, ...update.data }));
|
|
866
|
+
}
|
|
867
|
+
for (const slab of plan.create) {
|
|
868
|
+
slabsById.set(slab.id, slab);
|
|
869
|
+
}
|
|
870
|
+
return [...slabsById.values()];
|
|
871
|
+
}
|
|
872
|
+
export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context = {}) {
|
|
473
873
|
const manualCeilings = existingCeilings.filter((ceiling) => !ceiling.autoFromWalls);
|
|
474
874
|
const manualSignatures = new Set(manualCeilings.map((ceiling) => polygonSignature(ceiling.polygon.map(pointFromTuple))));
|
|
475
|
-
const
|
|
875
|
+
const manualPolygons = manualCeilings.map((ceiling) => ceiling.polygon.map(pointFromTuple));
|
|
876
|
+
const detectedAll = roomPolygons
|
|
476
877
|
.map((poly) => ({
|
|
477
878
|
poly: simplifyClosedPolygon(poly.map(pointToTuple), AUTO_SLAB_POLYGON_SIMPLIFY_TOLERANCE).map(pointFromTuple),
|
|
478
879
|
sig: '',
|
|
@@ -486,8 +887,9 @@ function syncAutoCeilingsForLevel(levelId, roomPolygons, existingCeilings, scene
|
|
|
486
887
|
centroid: polygonCentroid(room.poly),
|
|
487
888
|
area: Math.abs(polygonArea(room.poly)),
|
|
488
889
|
bbox: bboxOf(room.poly),
|
|
489
|
-
|
|
490
|
-
|
|
890
|
+
ceilingHeight: resolveAutoCeilingHeight(room.poly, context),
|
|
891
|
+
}));
|
|
892
|
+
const detected = detectedAll.filter(({ sig, poly }) => !manualSignatures.has(sig) && !matchesManualFootprint(poly, manualPolygons));
|
|
491
893
|
const existingAuto = existingCeilings.filter((ceiling) => ceiling.autoFromWalls);
|
|
492
894
|
const existingAutoMeta = existingAuto.map((ceiling) => {
|
|
493
895
|
const poly = ceiling.polygon.map(pointFromTuple);
|
|
@@ -504,15 +906,20 @@ function syncAutoCeilingsForLevel(levelId, roomPolygons, existingCeilings, scene
|
|
|
504
906
|
const updatesById = new Map();
|
|
505
907
|
const autoBySignature = new Map();
|
|
506
908
|
for (const entry of existingAutoMeta) {
|
|
507
|
-
autoBySignature.
|
|
909
|
+
const bucket = autoBySignature.get(entry.sig) ?? [];
|
|
910
|
+
bucket.push(entry);
|
|
911
|
+
autoBySignature.set(entry.sig, bucket);
|
|
508
912
|
}
|
|
509
913
|
detected.forEach((room, index) => {
|
|
510
|
-
const existing = autoBySignature.get(room.sig);
|
|
914
|
+
const existing = autoBySignature.get(room.sig)?.shift();
|
|
511
915
|
if (!existing)
|
|
512
916
|
return;
|
|
513
917
|
matchedDetectedIdx.add(index);
|
|
514
918
|
matchedCeilingIds.add(existing.ceiling.id);
|
|
515
|
-
updatesById.set(existing.ceiling.id,
|
|
919
|
+
updatesById.set(existing.ceiling.id, {
|
|
920
|
+
polygon: room.poly.map(pointToTuple),
|
|
921
|
+
height: room.ceilingHeight,
|
|
922
|
+
});
|
|
516
923
|
});
|
|
517
924
|
const remainingDetected = detected
|
|
518
925
|
.map((room, index) => ({ room, index }))
|
|
@@ -541,21 +948,44 @@ function syncAutoCeilingsForLevel(levelId, roomPolygons, existingCeilings, scene
|
|
|
541
948
|
continue;
|
|
542
949
|
matchedDetectedIdx.add(index);
|
|
543
950
|
matchedCeilingIds.add(bestMatch.entry.ceiling.id);
|
|
544
|
-
updatesById.set(bestMatch.entry.ceiling.id,
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
if (
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
951
|
+
updatesById.set(bestMatch.entry.ceiling.id, {
|
|
952
|
+
polygon: room.poly.map(pointToTuple),
|
|
953
|
+
height: room.ceilingHeight,
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
const detectedRoomPolygons = detectedAll.map((room) => room.poly);
|
|
957
|
+
const ceilingsToDelete = [];
|
|
958
|
+
const ceilingDemotions = [];
|
|
959
|
+
for (const ceiling of existingAuto) {
|
|
960
|
+
if (updatesById.has(ceiling.id))
|
|
961
|
+
continue;
|
|
962
|
+
const coverage = polygonCoverageRatio(ceiling.polygon.map(pointFromTuple), detectedRoomPolygons);
|
|
963
|
+
if (coverage >= ORPHAN_MERGE_COVERAGE_THRESHOLD) {
|
|
964
|
+
ceilingsToDelete.push(ceiling.id);
|
|
965
|
+
}
|
|
966
|
+
else {
|
|
967
|
+
ceilingDemotions.push({ id: ceiling.id, data: { autoFromWalls: false } });
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
const ceilingsToUpdate = [
|
|
971
|
+
...existingAuto
|
|
972
|
+
.filter((ceiling) => updatesById.has(ceiling.id))
|
|
973
|
+
.flatMap((ceiling) => {
|
|
974
|
+
const update = updatesById.get(ceiling.id);
|
|
975
|
+
if (!update)
|
|
976
|
+
return [];
|
|
977
|
+
const data = {};
|
|
978
|
+
if (!sameTuplePolygon(ceiling.polygon, update.polygon)) {
|
|
979
|
+
data.polygon = update.polygon;
|
|
980
|
+
}
|
|
981
|
+
if (Math.abs((ceiling.height ?? DEFAULT_AUTO_CEILING_HEIGHT) - update.height) >
|
|
982
|
+
CEILING_HEIGHT_EPSILON) {
|
|
983
|
+
data.height = update.height;
|
|
984
|
+
}
|
|
985
|
+
return Object.keys(data).length === 0 ? [] : [{ id: ceiling.id, data }];
|
|
986
|
+
}),
|
|
987
|
+
...ceilingDemotions,
|
|
988
|
+
];
|
|
559
989
|
const plannedCeilingsForNaming = [...existingCeilings];
|
|
560
990
|
const ceilingsToCreate = [];
|
|
561
991
|
for (let index = 0; index < detected.length; index += 1) {
|
|
@@ -570,29 +1000,38 @@ function syncAutoCeilingsForLevel(levelId, roomPolygons, existingCeilings, scene
|
|
|
570
1000
|
name,
|
|
571
1001
|
polygon: room.poly.map(pointToTuple),
|
|
572
1002
|
holes: [],
|
|
573
|
-
height:
|
|
1003
|
+
height: room.ceilingHeight,
|
|
574
1004
|
autoFromWalls: true,
|
|
575
1005
|
}));
|
|
576
1006
|
}
|
|
577
|
-
|
|
578
|
-
|
|
1007
|
+
return {
|
|
1008
|
+
create: ceilingsToCreate,
|
|
1009
|
+
update: ceilingsToUpdate,
|
|
1010
|
+
delete: ceilingsToDelete,
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
function syncAutoCeilingsForLevel(levelId, roomPolygons, existingCeilings, sceneStore, context = {}) {
|
|
1014
|
+
const plan = planAutoCeilingsForLevel(roomPolygons, existingCeilings, context);
|
|
1015
|
+
if (plan.delete.length > 0) {
|
|
1016
|
+
sceneStore.getState().deleteNodes(plan.delete);
|
|
579
1017
|
}
|
|
580
|
-
if (
|
|
581
|
-
sceneStore.getState().updateNodes(
|
|
1018
|
+
if (plan.update.length > 0) {
|
|
1019
|
+
sceneStore.getState().updateNodes(plan.update);
|
|
582
1020
|
}
|
|
583
|
-
if (
|
|
584
|
-
sceneStore.getState().createNodes(
|
|
1021
|
+
if (plan.create.length > 0) {
|
|
1022
|
+
sceneStore.getState().createNodes(plan.create.map((node) => ({ node, parentId: levelId })));
|
|
585
1023
|
}
|
|
586
1024
|
}
|
|
587
1025
|
function detectSpacesFromWalls(levelId, walls) {
|
|
588
|
-
const
|
|
1026
|
+
const rooms = extractRooms(walls);
|
|
1027
|
+
const roomPolygons = rooms.map((room) => room.polygon);
|
|
589
1028
|
const wallUpdates = walls.map((wall) => ({
|
|
590
1029
|
wallId: wall.id,
|
|
591
1030
|
...resolveWallSurfaceSides(wall, roomPolygons),
|
|
592
1031
|
}));
|
|
593
1032
|
return {
|
|
594
1033
|
roomPolygons,
|
|
595
|
-
spaces:
|
|
1034
|
+
spaces: rooms.map((room) => buildSpace(levelId, room)),
|
|
596
1035
|
wallUpdates,
|
|
597
1036
|
};
|
|
598
1037
|
}
|
|
@@ -612,6 +1051,7 @@ function runSpaceDetection(levelIds, sceneStore, editorStore, nodes) {
|
|
|
612
1051
|
const walls = Object.values(nodes).filter((node) => node?.type === 'wall' && node.parentId === levelId);
|
|
613
1052
|
const slabs = Object.values(nodes).filter((node) => node?.type === 'slab' && node.parentId === levelId);
|
|
614
1053
|
const ceilings = Object.values(nodes).filter((node) => node?.type === 'ceiling' && node.parentId === levelId);
|
|
1054
|
+
const zones = Object.values(nodes).filter((node) => node?.type === 'zone' && node.parentId === levelId);
|
|
615
1055
|
const { wallUpdates, spaces, roomPolygons } = detectSpacesFromWalls(levelId, walls);
|
|
616
1056
|
const changedWallUpdates = wallUpdates.filter((update) => {
|
|
617
1057
|
const wall = nodes[update.wallId];
|
|
@@ -626,16 +1066,45 @@ function runSpaceDetection(levelIds, sceneStore, editorStore, nodes) {
|
|
|
626
1066
|
},
|
|
627
1067
|
})));
|
|
628
1068
|
}
|
|
629
|
-
|
|
630
|
-
|
|
1069
|
+
const parsedSlabs = slabs.map((slab) => SlabNode.parse(slab));
|
|
1070
|
+
const slabPlan = syncAutoSlabsForLevel(levelId, roomPolygons, parsedSlabs, sceneStore);
|
|
1071
|
+
const projectedSlabs = projectAutoSlabsForPlan(parsedSlabs, slabPlan);
|
|
1072
|
+
syncAutoCeilingsForLevel(levelId, roomPolygons, ceilings.map((ceiling) => CeilingNode.parse(ceiling)), sceneStore, { walls, slabs: projectedSlabs });
|
|
1073
|
+
const zonePlan = planAutoZonesForLevel(spaces, zones.map((zone) => ZoneNode.parse(zone)));
|
|
1074
|
+
if (zonePlan.update.length > 0)
|
|
1075
|
+
updateNodes(zonePlan.update);
|
|
631
1076
|
for (const space of spaces) {
|
|
632
1077
|
nextSpaces[space.id] = space;
|
|
633
1078
|
}
|
|
634
1079
|
}
|
|
635
1080
|
editorStore.getState().setSpaces(nextSpaces);
|
|
636
1081
|
}
|
|
1082
|
+
// Refcount of outstanding pause requests, matching the pauseSceneHistory
|
|
1083
|
+
// pattern. The community editor flips this off while the AI is actively
|
|
1084
|
+
// mutating the scene so the wall-driven auto slab/ceiling sync doesn't race
|
|
1085
|
+
// `create_room`'s explicit slabs/ceilings (see plan
|
|
1086
|
+
// `ai-pause-space-detection`).
|
|
1087
|
+
let spaceDetectionPauseDepth = 0;
|
|
1088
|
+
/** Pause the wall-driven auto slab/ceiling sync. Refcounted — pair with `resumeSpaceDetection`. */
|
|
1089
|
+
export function pauseSpaceDetection() {
|
|
1090
|
+
spaceDetectionPauseDepth += 1;
|
|
1091
|
+
}
|
|
1092
|
+
/** Resume the wall-driven auto slab/ceiling sync. No-op if not currently paused. */
|
|
1093
|
+
export function resumeSpaceDetection() {
|
|
1094
|
+
if (spaceDetectionPauseDepth === 0)
|
|
1095
|
+
return;
|
|
1096
|
+
spaceDetectionPauseDepth -= 1;
|
|
1097
|
+
}
|
|
1098
|
+
/** True iff the wall-driven auto slab/ceiling sync is currently paused. */
|
|
1099
|
+
export function isSpaceDetectionPaused() {
|
|
1100
|
+
return spaceDetectionPauseDepth > 0;
|
|
1101
|
+
}
|
|
637
1102
|
export function initSpaceDetectionSync(sceneStore, editorStore) {
|
|
638
|
-
|
|
1103
|
+
// Baseline from whatever is already in the store. Detection reacts to wall
|
|
1104
|
+
// edits made IN-SESSION (create / move / delete); it must not re-litigate a
|
|
1105
|
+
// scene that merely loaded — rerunning on hydration resurrected auto slabs
|
|
1106
|
+
// the user had deleted in an earlier session.
|
|
1107
|
+
const previousSnapshots = levelStructureSnapshots(sceneStore.getState().nodes);
|
|
639
1108
|
let isProcessing = false;
|
|
640
1109
|
const unsubscribe = sceneStore.subscribe((state) => {
|
|
641
1110
|
if (isProcessing)
|
|
@@ -643,22 +1112,27 @@ export function initSpaceDetectionSync(sceneStore, editorStore) {
|
|
|
643
1112
|
if (getSceneHistoryPauseDepth() > 0)
|
|
644
1113
|
return;
|
|
645
1114
|
const nodes = state.nodes;
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
1115
|
+
const currentSnapshots = levelStructureSnapshots(nodes);
|
|
1116
|
+
// Paused: roll the snapshot forward so we don't backfill (and re-duplicate)
|
|
1117
|
+
// every paused change once detection resumes. Whatever the AI built while
|
|
1118
|
+
// paused becomes the new baseline; only future changes will reconcile.
|
|
1119
|
+
if (spaceDetectionPauseDepth > 0) {
|
|
1120
|
+
previousSnapshots.clear();
|
|
1121
|
+
for (const [levelId, snapshot] of currentSnapshots.entries()) {
|
|
1122
|
+
previousSnapshots.set(levelId, snapshot);
|
|
653
1123
|
}
|
|
654
|
-
|
|
655
|
-
const currentSnapshots = new Map();
|
|
656
|
-
for (const [levelId, walls] of wallsByLevel.entries()) {
|
|
657
|
-
currentSnapshots.set(levelId, levelWallSnapshot(walls));
|
|
1124
|
+
return;
|
|
658
1125
|
}
|
|
659
1126
|
const levelsToUpdate = new Set();
|
|
660
1127
|
for (const levelId of new Set([...previousSnapshots.keys(), ...currentSnapshots.keys()])) {
|
|
661
|
-
|
|
1128
|
+
// First sight of a level is a hydration baseline, not a wall edit —
|
|
1129
|
+
// `setScene` delivers a loaded scene as one atomic update, and a level's
|
|
1130
|
+
// first wall can't close a room anyway. Record it (below) and only
|
|
1131
|
+
// react to subsequent changes.
|
|
1132
|
+
const previous = previousSnapshots.get(levelId);
|
|
1133
|
+
if (previous === undefined)
|
|
1134
|
+
continue;
|
|
1135
|
+
if (previous !== (currentSnapshots.get(levelId) ?? '')) {
|
|
662
1136
|
levelsToUpdate.add(levelId);
|
|
663
1137
|
}
|
|
664
1138
|
}
|
|
@@ -677,7 +1151,8 @@ export function initSpaceDetectionSync(sceneStore, editorStore) {
|
|
|
677
1151
|
finally {
|
|
678
1152
|
resumeSceneHistory(sceneStore);
|
|
679
1153
|
previousSnapshots.clear();
|
|
680
|
-
|
|
1154
|
+
const postRunSnapshots = levelStructureSnapshots(sceneStore.getState().nodes);
|
|
1155
|
+
for (const [levelId, snapshot] of postRunSnapshots.entries()) {
|
|
681
1156
|
previousSnapshots.set(levelId, snapshot);
|
|
682
1157
|
}
|
|
683
1158
|
isProcessing = false;
|