@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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure alignment-guide resolver — no React, no DOM, no scene access.
|
|
3
|
+
*
|
|
4
|
+
* Given a moving object's anchor points at its proposed position and a
|
|
5
|
+
* pool of candidate anchors from nearby static objects, returns:
|
|
6
|
+
* - the best per-axis matches as `Guide` rendering primitives, and
|
|
7
|
+
* - an optional `{ dx, dz }` snap delta the caller can apply.
|
|
8
|
+
*
|
|
9
|
+
* Anchors are 2D points on the floor plane (XZ, in world meters). The
|
|
10
|
+
* resolver picks at most one match per axis: the smallest |Δx| match
|
|
11
|
+
* snaps X; the smallest |Δz| match snaps Z. This mirrors Figma's
|
|
12
|
+
* behaviour — guides appear along the matched axes, regardless of how
|
|
13
|
+
* many neighbours could have matched.
|
|
14
|
+
*
|
|
15
|
+
* Two guides max per call keeps the visual signal sharp at the cost of
|
|
16
|
+
* not surfacing every possible alignment at once. Multi-guide ("this
|
|
17
|
+
* lines up with three things") is intentionally out of scope for v1.
|
|
18
|
+
*/
|
|
19
|
+
const EMPTY = { guides: [], snap: null };
|
|
20
|
+
/** Forward rotation: local XZ → world XZ for a node whose parent has
|
|
21
|
+
* position `bx,_,bz` and rotation-Y `rotY` (radians). Matches the
|
|
22
|
+
* transform used throughout the editor's tools / floor-plan. */
|
|
23
|
+
function localToWorld(x, z, bx, bz, cos, sin) {
|
|
24
|
+
return {
|
|
25
|
+
x: bx + x * cos + z * sin,
|
|
26
|
+
z: bz - x * sin + z * cos,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function transformAnchorToWorld(anchor, bx, bz, cos, sin) {
|
|
30
|
+
const w = localToWorld(anchor.x, anchor.z, bx, bz, cos, sin);
|
|
31
|
+
return { nodeId: anchor.nodeId, kind: anchor.kind, x: w.x, z: w.z };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve alignment in WORLD space while accepting BUILDING-LOCAL anchors.
|
|
35
|
+
*
|
|
36
|
+
* Why this exists: the floor-plan grid lives in world XZ (rendered outside
|
|
37
|
+
* the rotated scene group), so alignment must follow the same axes —
|
|
38
|
+
* otherwise rotating a building drags the alignment guides off the visible
|
|
39
|
+
* grid and onto the rotated wall's local axes (the bug the user hit). The
|
|
40
|
+
* resolver itself is frame-agnostic; this wrapper just transforms anchors
|
|
41
|
+
* to world, resolves, then rotates the snap delta back into building-local
|
|
42
|
+
* so callers can add it to a local position without further math.
|
|
43
|
+
*
|
|
44
|
+
* `pose === null` → resolve in the caller's frame as-is (no transform).
|
|
45
|
+
*/
|
|
46
|
+
export function resolveAlignmentInBuildingWorld(input) {
|
|
47
|
+
const { moving, candidates, threshold, pose } = input;
|
|
48
|
+
if (!pose) {
|
|
49
|
+
return resolveAlignment({ moving, candidates, threshold });
|
|
50
|
+
}
|
|
51
|
+
const cos = Math.cos(pose.rotationY);
|
|
52
|
+
const sin = Math.sin(pose.rotationY);
|
|
53
|
+
const bx = pose.position[0];
|
|
54
|
+
const bz = pose.position[2];
|
|
55
|
+
const movingWorld = moving.map((a) => transformAnchorToWorld(a, bx, bz, cos, sin));
|
|
56
|
+
const candidatesWorld = candidates.map((a) => transformAnchorToWorld(a, bx, bz, cos, sin));
|
|
57
|
+
const result = resolveAlignment({
|
|
58
|
+
moving: movingWorld,
|
|
59
|
+
candidates: candidatesWorld,
|
|
60
|
+
threshold,
|
|
61
|
+
});
|
|
62
|
+
if (!result.snap)
|
|
63
|
+
return { guides: result.guides, snap: null };
|
|
64
|
+
// World → local rotation (orthogonal matrix → transpose). The inverse of
|
|
65
|
+
// `localToWorld` above maps (dx_world, dz_world) → (dx_local, dz_local).
|
|
66
|
+
const dxW = result.snap.dx;
|
|
67
|
+
const dzW = result.snap.dz;
|
|
68
|
+
const dxL = dxW * cos - dzW * sin;
|
|
69
|
+
const dzL = dxW * sin + dzW * cos;
|
|
70
|
+
return { guides: result.guides, snap: { dx: dxL, dz: dzL } };
|
|
71
|
+
}
|
|
72
|
+
export function resolveAlignment(input) {
|
|
73
|
+
const { moving, candidates, threshold } = input;
|
|
74
|
+
if (threshold <= 0 || moving.length === 0 || candidates.length === 0)
|
|
75
|
+
return EMPTY;
|
|
76
|
+
let bestX = null;
|
|
77
|
+
let bestZ = null;
|
|
78
|
+
for (const m of moving) {
|
|
79
|
+
for (const c of candidates) {
|
|
80
|
+
const dx = c.x - m.x;
|
|
81
|
+
const dz = c.z - m.z;
|
|
82
|
+
const adx = Math.abs(dx);
|
|
83
|
+
const adz = Math.abs(dz);
|
|
84
|
+
if (adx <= threshold &&
|
|
85
|
+
(bestX === null || adz < bestX.perp || (adz === bestX.perp && adx < bestX.primary))) {
|
|
86
|
+
bestX = { delta: dx, primary: adx, perp: adz, m, c };
|
|
87
|
+
}
|
|
88
|
+
if (adz <= threshold &&
|
|
89
|
+
(bestZ === null || adx < bestZ.perp || (adx === bestZ.perp && adz < bestZ.primary))) {
|
|
90
|
+
bestZ = { delta: dz, primary: adz, perp: adx, m, c };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!bestX && !bestZ)
|
|
95
|
+
return EMPTY;
|
|
96
|
+
const dxSnap = bestX?.delta ?? 0;
|
|
97
|
+
const dzSnap = bestZ?.delta ?? 0;
|
|
98
|
+
const guides = [];
|
|
99
|
+
if (bestX) {
|
|
100
|
+
// X-axis match: vertical guide at x = bestX.c.x. The moving anchor
|
|
101
|
+
// ends up at (c.x, m.z + dzSnap). Span the line between them.
|
|
102
|
+
const snappedMz = bestX.m.z + dzSnap;
|
|
103
|
+
const z1 = Math.min(bestX.c.z, snappedMz);
|
|
104
|
+
const z2 = Math.max(bestX.c.z, snappedMz);
|
|
105
|
+
guides.push({
|
|
106
|
+
axis: 'x',
|
|
107
|
+
coord: bestX.c.x,
|
|
108
|
+
from: { x: bestX.c.x, z: z1 },
|
|
109
|
+
to: { x: bestX.c.x, z: z2 },
|
|
110
|
+
anchor: { x: bestX.c.x, z: bestX.c.z },
|
|
111
|
+
movingAnchorKind: bestX.m.kind,
|
|
112
|
+
candidateAnchorKind: bestX.c.kind,
|
|
113
|
+
candidateNodeId: bestX.c.nodeId,
|
|
114
|
+
distance: Math.abs(snappedMz - bestX.c.z),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (bestZ) {
|
|
118
|
+
const snappedMx = bestZ.m.x + dxSnap;
|
|
119
|
+
const x1 = Math.min(bestZ.c.x, snappedMx);
|
|
120
|
+
const x2 = Math.max(bestZ.c.x, snappedMx);
|
|
121
|
+
guides.push({
|
|
122
|
+
axis: 'z',
|
|
123
|
+
coord: bestZ.c.z,
|
|
124
|
+
from: { x: x1, z: bestZ.c.z },
|
|
125
|
+
to: { x: x2, z: bestZ.c.z },
|
|
126
|
+
anchor: { x: bestZ.c.x, z: bestZ.c.z },
|
|
127
|
+
movingAnchorKind: bestZ.m.kind,
|
|
128
|
+
candidateAnchorKind: bestZ.c.kind,
|
|
129
|
+
candidateNodeId: bestZ.c.nodeId,
|
|
130
|
+
distance: Math.abs(snappedMx - bestZ.c.x),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return { guides, snap: { dx: dxSnap, dz: dzSnap } };
|
|
134
|
+
}
|
|
135
|
+
// ─── Anchor extractors (pure) ─────────────────────────────────────────
|
|
136
|
+
/**
|
|
137
|
+
* Produces the 9 standard anchors for an axis-aligned bounding box on the
|
|
138
|
+
* floor plane: 4 corners, 4 edge midpoints, 1 center. Suitable for any
|
|
139
|
+
* floor-plan entity whose footprint can be expressed as a bbox.
|
|
140
|
+
*
|
|
141
|
+
* Caller is responsible for computing the bbox — the resolver doesn't
|
|
142
|
+
* care how (per-kind dimensions, SVG getBBox(), etc.).
|
|
143
|
+
*/
|
|
144
|
+
export function bboxAnchors(nodeId, minX, minZ, maxX, maxZ) {
|
|
145
|
+
const cx = (minX + maxX) / 2;
|
|
146
|
+
const cz = (minZ + maxZ) / 2;
|
|
147
|
+
return [
|
|
148
|
+
{ nodeId, kind: 'corner', x: minX, z: minZ },
|
|
149
|
+
{ nodeId, kind: 'corner', x: maxX, z: minZ },
|
|
150
|
+
{ nodeId, kind: 'corner', x: maxX, z: maxZ },
|
|
151
|
+
{ nodeId, kind: 'corner', x: minX, z: maxZ },
|
|
152
|
+
{ nodeId, kind: 'edge-mid', x: cx, z: minZ },
|
|
153
|
+
{ nodeId, kind: 'edge-mid', x: maxX, z: cz },
|
|
154
|
+
{ nodeId, kind: 'edge-mid', x: cx, z: maxZ },
|
|
155
|
+
{ nodeId, kind: 'edge-mid', x: minX, z: cz },
|
|
156
|
+
{ nodeId, kind: 'center', x: cx, z: cz },
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The 4 corner anchors of a bbox — edges only, no edge-midpoints or center.
|
|
161
|
+
* Used where alignment should lock to an object's edges (left/right/front/
|
|
162
|
+
* back), never its centreline.
|
|
163
|
+
*/
|
|
164
|
+
export function bboxCornerAnchors(nodeId, minX, minZ, maxX, maxZ) {
|
|
165
|
+
return [
|
|
166
|
+
{ nodeId, kind: 'corner', x: minX, z: minZ },
|
|
167
|
+
{ nodeId, kind: 'corner', x: maxX, z: minZ },
|
|
168
|
+
{ nodeId, kind: 'corner', x: maxX, z: maxZ },
|
|
169
|
+
{ nodeId, kind: 'corner', x: minX, z: maxZ },
|
|
170
|
+
];
|
|
171
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alignment.test.d.ts","sourceRoot":"","sources":["../../src/services/alignment.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { bboxAnchors, resolveAlignment } from './alignment';
|
|
3
|
+
function center(nodeId, x, z) {
|
|
4
|
+
return { nodeId, kind: 'center', x, z };
|
|
5
|
+
}
|
|
6
|
+
function corner(nodeId, x, z) {
|
|
7
|
+
return { nodeId, kind: 'corner', x, z };
|
|
8
|
+
}
|
|
9
|
+
describe('resolveAlignment', () => {
|
|
10
|
+
test('returns empty when no candidates within threshold', () => {
|
|
11
|
+
const result = resolveAlignment({
|
|
12
|
+
moving: [center('m', 0, 0)],
|
|
13
|
+
candidates: [center('a', 1, 1)],
|
|
14
|
+
threshold: 0.5,
|
|
15
|
+
});
|
|
16
|
+
expect(result.guides).toEqual([]);
|
|
17
|
+
expect(result.snap).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
test('snaps moving anchor onto candidate X when within threshold', () => {
|
|
20
|
+
const result = resolveAlignment({
|
|
21
|
+
moving: [center('m', 0.03, 5)],
|
|
22
|
+
candidates: [center('a', 0, 2)],
|
|
23
|
+
threshold: 0.1,
|
|
24
|
+
});
|
|
25
|
+
expect(result.snap).toEqual({ dx: -0.03, dz: 0 });
|
|
26
|
+
expect(result.guides).toHaveLength(1);
|
|
27
|
+
expect(result.guides[0].axis).toBe('x');
|
|
28
|
+
expect(result.guides[0].coord).toBe(0);
|
|
29
|
+
expect(result.guides[0].from.z).toBe(2);
|
|
30
|
+
expect(result.guides[0].to.z).toBe(5);
|
|
31
|
+
});
|
|
32
|
+
test('snaps both axes when both match', () => {
|
|
33
|
+
const result = resolveAlignment({
|
|
34
|
+
moving: [center('m', 0.03, 0.04)],
|
|
35
|
+
candidates: [center('a', 0, 0)],
|
|
36
|
+
threshold: 0.1,
|
|
37
|
+
});
|
|
38
|
+
expect(result.snap).toEqual({ dx: -0.03, dz: -0.04 });
|
|
39
|
+
expect(result.guides).toHaveLength(2);
|
|
40
|
+
});
|
|
41
|
+
test('picks closest candidate per axis', () => {
|
|
42
|
+
const result = resolveAlignment({
|
|
43
|
+
moving: [center('m', 0.08, 0)],
|
|
44
|
+
candidates: [center('a', 0, 5), center('b', 0.1, 5), center('c', 0.05, 10)],
|
|
45
|
+
threshold: 0.1,
|
|
46
|
+
});
|
|
47
|
+
// |0.1 - 0.08| = 0.02 wins over |0.05 - 0.08| = 0.03 and |0 - 0.08| = 0.08
|
|
48
|
+
expect(result.snap?.dx).toBeCloseTo(0.02, 10);
|
|
49
|
+
expect(result.guides[0].candidateNodeId).toBe('b');
|
|
50
|
+
});
|
|
51
|
+
test('ties on the matched axis break toward the nearest perpendicular anchor', () => {
|
|
52
|
+
const result = resolveAlignment({
|
|
53
|
+
moving: [corner('m', 0.02, 4)],
|
|
54
|
+
candidates: [corner('far', 0, 0), corner('near', 0, 5)],
|
|
55
|
+
threshold: 0.1,
|
|
56
|
+
});
|
|
57
|
+
// Both share X (Δx = 0.02); 'near' (z=5) is closer to the moving z=4 than
|
|
58
|
+
// 'far' (z=0), so the guide connects to the nearest real anchor.
|
|
59
|
+
expect(result.guides[0].candidateNodeId).toBe('near');
|
|
60
|
+
});
|
|
61
|
+
test('threshold = 0 disables alignment', () => {
|
|
62
|
+
const result = resolveAlignment({
|
|
63
|
+
moving: [center('m', 0, 0)],
|
|
64
|
+
candidates: [center('a', 0, 0)],
|
|
65
|
+
threshold: 0,
|
|
66
|
+
});
|
|
67
|
+
expect(result.guides).toEqual([]);
|
|
68
|
+
expect(result.snap).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
test('distance is the perpendicular gap to the matched axis', () => {
|
|
71
|
+
const result = resolveAlignment({
|
|
72
|
+
moving: [center('m', 0.02, 3)],
|
|
73
|
+
candidates: [center('a', 0, 0)],
|
|
74
|
+
threshold: 0.1,
|
|
75
|
+
});
|
|
76
|
+
// After snap: moving at (0, 3). X guide runs along x=0 from z=0 to z=3.
|
|
77
|
+
expect(result.guides[0].distance).toBeCloseTo(3, 10);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('bboxAnchors', () => {
|
|
81
|
+
test('returns 9 anchors with correct kinds and positions', () => {
|
|
82
|
+
const anchors = bboxAnchors('node', 0, 0, 2, 4);
|
|
83
|
+
expect(anchors).toHaveLength(9);
|
|
84
|
+
const corners = anchors.filter((a) => a.kind === 'corner');
|
|
85
|
+
const edges = anchors.filter((a) => a.kind === 'edge-mid');
|
|
86
|
+
const centers = anchors.filter((a) => a.kind === 'center');
|
|
87
|
+
expect(corners).toHaveLength(4);
|
|
88
|
+
expect(edges).toHaveLength(4);
|
|
89
|
+
expect(centers).toHaveLength(1);
|
|
90
|
+
expect(centers[0]).toEqual({ nodeId: 'node', kind: 'center', x: 1, z: 2 });
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type ChildQuery, type SpatialQuery } from '../registry/relations-resolver';
|
|
2
|
+
import type { DragAction, Modifiers, SceneApi } from '../registry/types';
|
|
3
|
+
import type { AnyNode } from '../schema/types';
|
|
4
|
+
import type { Vec2 } from './snap';
|
|
5
|
+
/**
|
|
6
|
+
* Pure orchestrator for a single `DragAction` lifecycle:
|
|
7
|
+
* begin → (preview → snap? → apply → cascade dirty)* → commit | cancel
|
|
8
|
+
*
|
|
9
|
+
* Bracketed by `pauseHistory()` / `resumeHistory()` so the entire drag is one
|
|
10
|
+
* undo step. The React hook (`useDragAction` in `@pascal-app/editor`) wraps
|
|
11
|
+
* this with event subscriptions; tests drive it directly.
|
|
12
|
+
*/
|
|
13
|
+
export type DragSessionInput = {
|
|
14
|
+
node?: AnyNode;
|
|
15
|
+
point: Vec2;
|
|
16
|
+
handleId?: string;
|
|
17
|
+
modifiers?: Modifiers;
|
|
18
|
+
};
|
|
19
|
+
export type DragSessionOptions = {
|
|
20
|
+
spatialQuery?: SpatialQuery;
|
|
21
|
+
childQuery?: ChildQuery;
|
|
22
|
+
/** Called once the session terminates via `commit()`. */
|
|
23
|
+
onCommit?: () => void;
|
|
24
|
+
/** Called once the session terminates via `cancel()` or `dispose()`. */
|
|
25
|
+
onCancel?: () => void;
|
|
26
|
+
};
|
|
27
|
+
export type DragSession<Ctx, Draft> = {
|
|
28
|
+
/** Begin the drag — pause history, capture ctx via `action.begin`. */
|
|
29
|
+
start: (input: DragSessionInput) => void;
|
|
30
|
+
/** Per-pointer-move tick — run preview/snap/apply and cascade dirty marks. */
|
|
31
|
+
move: (point: Vec2, modifiers: Modifiers) => void;
|
|
32
|
+
/** Pointer-up / discrete commit. Returns true if `action.commit` agreed. */
|
|
33
|
+
commit: () => boolean;
|
|
34
|
+
/** Pointer-cancel / Esc / external abort — restores all touched nodes. */
|
|
35
|
+
cancel: () => void;
|
|
36
|
+
/** Returns the latest draft `apply` produced (or null before first move). */
|
|
37
|
+
getDraft: () => Draft | null;
|
|
38
|
+
isActive: () => boolean;
|
|
39
|
+
/** Idempotent cleanup. If active, restores scene state and resumes
|
|
40
|
+
* history, but does **not** fire `onCancel`. Use for React-effect
|
|
41
|
+
* teardown — onCancel would re-trigger the parent's state machine and
|
|
42
|
+
* break StrictMode's double-mount cycle. Esc / external aborts must
|
|
43
|
+
* still call `cancel()` directly. */
|
|
44
|
+
dispose: () => void;
|
|
45
|
+
};
|
|
46
|
+
export declare function createDragSession<Ctx, Draft>(action: DragAction<Ctx, Draft>, scene: SceneApi, options?: DragSessionOptions): DragSession<Ctx, Draft>;
|
|
47
|
+
//# sourceMappingURL=drag-session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drag-session.d.ts","sourceRoot":"","sources":["../../src/services/drag-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAgB,KAAK,YAAY,EAAE,MAAM,gCAAgC,CAAA;AACjG,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACxE,OAAO,KAAK,EAAE,OAAO,EAAa,MAAM,iBAAiB,CAAA;AACzD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAElC;;;;;;;GAOG;AAEH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,KAAK,EAAE,IAAI,CAAA;IACX,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,YAAY,CAAC,EAAE,YAAY,CAAA;IAC3B,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,WAAW,CAAC,GAAG,EAAE,KAAK,IAAI;IACpC,sEAAsE;IACtE,KAAK,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAA;IACxC,8EAA8E;IAC9E,IAAI,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,KAAK,IAAI,CAAA;IACjD,4EAA4E;IAC5E,MAAM,EAAE,MAAM,OAAO,CAAA;IACrB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,KAAK,GAAG,IAAI,CAAA;IAC5B,QAAQ,EAAE,MAAM,OAAO,CAAA;IACvB;;;;yCAIqC;IACrC,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,CAAA;AAID,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,KAAK,EAC1C,MAAM,EAAE,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,EAC9B,KAAK,EAAE,QAAQ,EACf,OAAO,GAAE,kBAAuB,GAC/B,WAAW,CAAC,GAAG,EAAE,KAAK,CAAC,CAiGzB"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { cascadeDirty } from '../registry/relations-resolver';
|
|
2
|
+
const EMPTY_MODIFIERS = { shift: false, alt: false, ctrl: false, meta: false };
|
|
3
|
+
export function createDragSession(action, scene, options = {}) {
|
|
4
|
+
let active = false;
|
|
5
|
+
let ctx = null;
|
|
6
|
+
let draft = null;
|
|
7
|
+
let dirtyMarked = new Set();
|
|
8
|
+
function markWithCascade(id) {
|
|
9
|
+
if (dirtyMarked.has(id))
|
|
10
|
+
return;
|
|
11
|
+
const ids = cascadeDirty(id, {
|
|
12
|
+
scene,
|
|
13
|
+
spatialQuery: options.spatialQuery,
|
|
14
|
+
childQuery: options.childQuery,
|
|
15
|
+
});
|
|
16
|
+
for (const dirtyId of ids) {
|
|
17
|
+
if (!dirtyMarked.has(dirtyId)) {
|
|
18
|
+
scene.markDirty(dirtyId);
|
|
19
|
+
dirtyMarked.add(dirtyId);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function terminate(committed) {
|
|
24
|
+
if (!active)
|
|
25
|
+
return;
|
|
26
|
+
active = false;
|
|
27
|
+
ctx = null;
|
|
28
|
+
draft = null;
|
|
29
|
+
dirtyMarked = new Set();
|
|
30
|
+
scene.resumeHistory();
|
|
31
|
+
if (committed)
|
|
32
|
+
options.onCommit?.();
|
|
33
|
+
else
|
|
34
|
+
options.onCancel?.();
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
start(input) {
|
|
38
|
+
if (active)
|
|
39
|
+
return; // ignore re-entry
|
|
40
|
+
scene.pauseHistory();
|
|
41
|
+
ctx = action.begin({
|
|
42
|
+
node: input.node,
|
|
43
|
+
point: input.point,
|
|
44
|
+
handleId: input.handleId,
|
|
45
|
+
modifiers: input.modifiers ?? EMPTY_MODIFIERS,
|
|
46
|
+
});
|
|
47
|
+
active = true;
|
|
48
|
+
},
|
|
49
|
+
move(point, modifiers) {
|
|
50
|
+
if (!active || ctx == null)
|
|
51
|
+
return;
|
|
52
|
+
let next = action.preview(ctx, point, modifiers);
|
|
53
|
+
if (action.snap) {
|
|
54
|
+
next = action.snap(next, ctx, undefined);
|
|
55
|
+
}
|
|
56
|
+
draft = next;
|
|
57
|
+
const dirtyIds = action.apply(next, ctx, scene);
|
|
58
|
+
for (const id of dirtyIds)
|
|
59
|
+
markWithCascade(id);
|
|
60
|
+
},
|
|
61
|
+
commit() {
|
|
62
|
+
if (!active || ctx == null)
|
|
63
|
+
return false;
|
|
64
|
+
const ok = action.commit?.(draft, ctx, scene) ?? true;
|
|
65
|
+
if (!ok) {
|
|
66
|
+
action.cancel(ctx, scene);
|
|
67
|
+
scene.restoreAll();
|
|
68
|
+
}
|
|
69
|
+
terminate(ok);
|
|
70
|
+
return ok;
|
|
71
|
+
},
|
|
72
|
+
cancel() {
|
|
73
|
+
if (!active || ctx == null)
|
|
74
|
+
return;
|
|
75
|
+
action.cancel(ctx, scene);
|
|
76
|
+
scene.restoreAll();
|
|
77
|
+
terminate(false);
|
|
78
|
+
},
|
|
79
|
+
getDraft() {
|
|
80
|
+
return draft;
|
|
81
|
+
},
|
|
82
|
+
isActive() {
|
|
83
|
+
return active;
|
|
84
|
+
},
|
|
85
|
+
dispose() {
|
|
86
|
+
if (active && ctx != null) {
|
|
87
|
+
action.cancel(ctx, scene);
|
|
88
|
+
scene.restoreAll();
|
|
89
|
+
// Silent terminate: no onCancel. The caller (e.g. useDragAction's
|
|
90
|
+
// effect cleanup) is reacting to the parent unmounting and would
|
|
91
|
+
// loop the state machine if onCancel re-set the parent's state.
|
|
92
|
+
active = false;
|
|
93
|
+
ctx = null;
|
|
94
|
+
draft = null;
|
|
95
|
+
dirtyMarked = new Set();
|
|
96
|
+
scene.resumeHistory();
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drag-session.test.d.ts","sourceRoot":"","sources":["../../src/services/drag-session.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { nodeRegistry, registerNode } from '../registry/registry';
|
|
4
|
+
import { createDragSession } from './drag-session';
|
|
5
|
+
const id = (s) => s;
|
|
6
|
+
function makeSpyScene(initial = {}) {
|
|
7
|
+
const calls = {
|
|
8
|
+
pauseHistory: 0,
|
|
9
|
+
resumeHistory: 0,
|
|
10
|
+
restoreAll: 0,
|
|
11
|
+
markedDirty: [],
|
|
12
|
+
updated: [],
|
|
13
|
+
};
|
|
14
|
+
const nodes = { ...initial };
|
|
15
|
+
return {
|
|
16
|
+
get: ((nid) => nodes[nid]),
|
|
17
|
+
nodes: () => nodes,
|
|
18
|
+
update: (nid, patch) => {
|
|
19
|
+
calls.updated.push([nid, patch]);
|
|
20
|
+
const existing = nodes[nid];
|
|
21
|
+
if (existing)
|
|
22
|
+
nodes[nid] = { ...existing, ...patch };
|
|
23
|
+
},
|
|
24
|
+
upsert: (n) => {
|
|
25
|
+
nodes[n.id] = n;
|
|
26
|
+
return n.id;
|
|
27
|
+
},
|
|
28
|
+
delete: (nid) => {
|
|
29
|
+
delete nodes[nid];
|
|
30
|
+
},
|
|
31
|
+
restore: () => { },
|
|
32
|
+
restoreAll: () => {
|
|
33
|
+
calls.restoreAll += 1;
|
|
34
|
+
},
|
|
35
|
+
markDirty: (nid) => {
|
|
36
|
+
calls.markedDirty.push(nid);
|
|
37
|
+
},
|
|
38
|
+
pauseHistory: () => {
|
|
39
|
+
calls.pauseHistory += 1;
|
|
40
|
+
},
|
|
41
|
+
resumeHistory: () => {
|
|
42
|
+
calls.resumeHistory += 1;
|
|
43
|
+
},
|
|
44
|
+
getSubtree: () => null,
|
|
45
|
+
cloneNodesInto: () => null,
|
|
46
|
+
_calls: calls,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function makeDef(kind, relations) {
|
|
50
|
+
return {
|
|
51
|
+
kind,
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
schema: z.object({ type: z.literal(kind) }),
|
|
54
|
+
category: 'utility',
|
|
55
|
+
defaults: () => ({}),
|
|
56
|
+
capabilities: {},
|
|
57
|
+
relations,
|
|
58
|
+
renderer: { kind: 'parametric', module: async () => ({ default: () => null }) },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function makeAction() {
|
|
62
|
+
return {
|
|
63
|
+
begin: ({ node }) => ({ id: node?.id ?? id('default') }),
|
|
64
|
+
preview: (_ctx, point) => ({ x: point[0] }),
|
|
65
|
+
apply: (_draft, ctx) => [ctx.id],
|
|
66
|
+
cancel: () => { },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
describe('createDragSession', () => {
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
nodeRegistry._reset();
|
|
72
|
+
});
|
|
73
|
+
test('start pauses history; commit resumes', () => {
|
|
74
|
+
const scene = makeSpyScene();
|
|
75
|
+
const session = createDragSession(makeAction(), scene);
|
|
76
|
+
session.start({ point: [0, 0] });
|
|
77
|
+
expect(scene._calls.pauseHistory).toBe(1);
|
|
78
|
+
expect(scene._calls.resumeHistory).toBe(0);
|
|
79
|
+
session.commit();
|
|
80
|
+
expect(scene._calls.resumeHistory).toBe(1);
|
|
81
|
+
});
|
|
82
|
+
test('cancel resumes history and calls restoreAll', () => {
|
|
83
|
+
const scene = makeSpyScene();
|
|
84
|
+
const session = createDragSession(makeAction(), scene);
|
|
85
|
+
session.start({ point: [0, 0] });
|
|
86
|
+
session.cancel();
|
|
87
|
+
expect(scene._calls.resumeHistory).toBe(1);
|
|
88
|
+
expect(scene._calls.restoreAll).toBe(1);
|
|
89
|
+
});
|
|
90
|
+
test('move runs preview + apply and marks the returned id dirty', () => {
|
|
91
|
+
const scene = makeSpyScene({ a: { id: id('a'), type: 'thing' } });
|
|
92
|
+
const session = createDragSession(makeAction(), scene);
|
|
93
|
+
session.start({ point: [0, 0], node: { id: id('a') } });
|
|
94
|
+
session.move([1, 0], { shift: false, alt: false, ctrl: false, meta: false });
|
|
95
|
+
expect(session.getDraft()).toEqual({ x: 1 });
|
|
96
|
+
expect(scene._calls.markedDirty).toContain(id('a'));
|
|
97
|
+
});
|
|
98
|
+
test('snap callback is invoked when defined', () => {
|
|
99
|
+
const action = {
|
|
100
|
+
...makeAction(),
|
|
101
|
+
snap: (draft) => ({ x: Math.round(draft.x) }),
|
|
102
|
+
};
|
|
103
|
+
const scene = makeSpyScene({ a: { id: id('a'), type: 'thing' } });
|
|
104
|
+
const session = createDragSession(action, scene);
|
|
105
|
+
session.start({ point: [0, 0], node: { id: id('a') } });
|
|
106
|
+
session.move([0.7, 0], { shift: false, alt: false, ctrl: false, meta: false });
|
|
107
|
+
expect(session.getDraft()).toEqual({ x: 1 });
|
|
108
|
+
});
|
|
109
|
+
test('commit returns false when action.commit returns false; calls action.cancel and restoreAll', () => {
|
|
110
|
+
const cancelSpy = mock(() => { });
|
|
111
|
+
const action = {
|
|
112
|
+
...makeAction(),
|
|
113
|
+
cancel: cancelSpy,
|
|
114
|
+
commit: () => false,
|
|
115
|
+
};
|
|
116
|
+
const scene = makeSpyScene({ a: { id: id('a'), type: 'thing' } });
|
|
117
|
+
const session = createDragSession(action, scene);
|
|
118
|
+
session.start({ point: [0, 0], node: { id: id('a') } });
|
|
119
|
+
session.move([1, 0], { shift: false, alt: false, ctrl: false, meta: false });
|
|
120
|
+
const result = session.commit();
|
|
121
|
+
expect(result).toBe(false);
|
|
122
|
+
expect(cancelSpy).toHaveBeenCalledTimes(1);
|
|
123
|
+
expect(scene._calls.restoreAll).toBe(1);
|
|
124
|
+
});
|
|
125
|
+
test('move is a no-op when session is not active', () => {
|
|
126
|
+
const scene = makeSpyScene();
|
|
127
|
+
const session = createDragSession(makeAction(), scene);
|
|
128
|
+
session.move([1, 0], { shift: false, alt: false, ctrl: false, meta: false });
|
|
129
|
+
expect(scene._calls.markedDirty.length).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
test('repeated start is a no-op (re-entry guard)', () => {
|
|
132
|
+
const scene = makeSpyScene();
|
|
133
|
+
const session = createDragSession(makeAction(), scene);
|
|
134
|
+
session.start({ point: [0, 0] });
|
|
135
|
+
session.start({ point: [99, 99] });
|
|
136
|
+
expect(scene._calls.pauseHistory).toBe(1); // only one pause
|
|
137
|
+
});
|
|
138
|
+
test('dispose mid-drag cancels and cleans up', () => {
|
|
139
|
+
const scene = makeSpyScene();
|
|
140
|
+
const session = createDragSession(makeAction(), scene);
|
|
141
|
+
session.start({ point: [0, 0] });
|
|
142
|
+
expect(session.isActive()).toBe(true);
|
|
143
|
+
session.dispose();
|
|
144
|
+
expect(session.isActive()).toBe(false);
|
|
145
|
+
expect(scene._calls.resumeHistory).toBe(1);
|
|
146
|
+
expect(scene._calls.restoreAll).toBe(1);
|
|
147
|
+
});
|
|
148
|
+
test('dispose when inactive is a no-op', () => {
|
|
149
|
+
const scene = makeSpyScene();
|
|
150
|
+
const session = createDragSession(makeAction(), scene);
|
|
151
|
+
session.dispose();
|
|
152
|
+
expect(scene._calls.pauseHistory).toBe(0);
|
|
153
|
+
expect(scene._calls.resumeHistory).toBe(0);
|
|
154
|
+
});
|
|
155
|
+
test('onCommit callback fires on successful commit', () => {
|
|
156
|
+
const onCommit = mock(() => { });
|
|
157
|
+
const onCancel = mock(() => { });
|
|
158
|
+
const scene = makeSpyScene();
|
|
159
|
+
const session = createDragSession(makeAction(), scene, { onCommit, onCancel });
|
|
160
|
+
session.start({ point: [0, 0] });
|
|
161
|
+
session.commit();
|
|
162
|
+
expect(onCommit).toHaveBeenCalledTimes(1);
|
|
163
|
+
expect(onCancel).toHaveBeenCalledTimes(0);
|
|
164
|
+
});
|
|
165
|
+
test('onCancel callback fires on explicit cancel', () => {
|
|
166
|
+
const onCommit = mock(() => { });
|
|
167
|
+
const onCancel = mock(() => { });
|
|
168
|
+
const scene = makeSpyScene();
|
|
169
|
+
const session = createDragSession(makeAction(), scene, { onCommit, onCancel });
|
|
170
|
+
session.start({ point: [0, 0] });
|
|
171
|
+
session.cancel();
|
|
172
|
+
expect(onCommit).toHaveBeenCalledTimes(0);
|
|
173
|
+
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
174
|
+
});
|
|
175
|
+
test('dispose does NOT fire onCancel (silent cleanup)', () => {
|
|
176
|
+
const onCommit = mock(() => { });
|
|
177
|
+
const onCancel = mock(() => { });
|
|
178
|
+
const scene = makeSpyScene();
|
|
179
|
+
const session = createDragSession(makeAction(), scene, { onCommit, onCancel });
|
|
180
|
+
session.start({ point: [0, 0] });
|
|
181
|
+
session.dispose();
|
|
182
|
+
expect(onCommit).toHaveBeenCalledTimes(0);
|
|
183
|
+
expect(onCancel).toHaveBeenCalledTimes(0);
|
|
184
|
+
expect(scene._calls.resumeHistory).toBe(1);
|
|
185
|
+
expect(scene._calls.restoreAll).toBe(1);
|
|
186
|
+
});
|
|
187
|
+
test('dirty cascade fires once per id even across multiple move ticks', () => {
|
|
188
|
+
// Register a kind with no relations — cascade returns just {startId}.
|
|
189
|
+
registerNode(makeDef('thing'));
|
|
190
|
+
const scene = makeSpyScene({ a: { id: id('a'), type: 'thing' } });
|
|
191
|
+
const session = createDragSession(makeAction(), scene);
|
|
192
|
+
session.start({ point: [0, 0], node: { id: id('a') } });
|
|
193
|
+
session.move([1, 0], { shift: false, alt: false, ctrl: false, meta: false });
|
|
194
|
+
session.move([2, 0], { shift: false, alt: false, ctrl: false, meta: false });
|
|
195
|
+
session.move([3, 0], { shift: false, alt: false, ctrl: false, meta: false });
|
|
196
|
+
// a is marked once, not three times
|
|
197
|
+
expect(scene._calls.markedDirty.filter((mid) => mid === id('a')).length).toBe(1);
|
|
198
|
+
});
|
|
199
|
+
test('dirty cascade follows hosts relations from the registry', () => {
|
|
200
|
+
registerNode(makeDef('wall', { hosts: ['door'] }));
|
|
201
|
+
registerNode(makeDef('door'));
|
|
202
|
+
const scene = makeSpyScene({
|
|
203
|
+
w: { id: id('w'), type: 'wall', children: [id('d')] },
|
|
204
|
+
d: { id: id('d'), type: 'door', parentId: id('w') },
|
|
205
|
+
});
|
|
206
|
+
const action = {
|
|
207
|
+
begin: () => ({ id: id('w') }),
|
|
208
|
+
preview: (_ctx, point) => ({ x: point[0] }),
|
|
209
|
+
apply: (_draft, ctx) => [ctx.id],
|
|
210
|
+
cancel: () => { },
|
|
211
|
+
};
|
|
212
|
+
const session = createDragSession(action, scene);
|
|
213
|
+
session.start({ point: [0, 0], node: { id: id('w') } });
|
|
214
|
+
session.move([1, 0], { shift: false, alt: false, ctrl: false, meta: false });
|
|
215
|
+
// both wall and door marked dirty
|
|
216
|
+
expect(scene._calls.markedDirty).toContain(id('w'));
|
|
217
|
+
expect(scene._calls.markedDirty).toContain(id('d'));
|
|
218
|
+
});
|
|
219
|
+
});
|