@pascal-app/editor 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/package.json +15 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +795 -28
- package/src/components/editor/editor-layout-mobile.tsx +8 -3
- package/src/components/editor/editor-layout-v2.tsx +76 -51
- package/src/components/editor/export-manager.tsx +69 -32
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
- package/src/components/editor/first-person/build-collider-world.ts +228 -83
- package/src/components/editor/first-person-controls.tsx +943 -27
- package/src/components/editor/floating-action-menu.tsx +676 -208
- package/src/components/editor/floating-building-action-menu.tsx +2 -2
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +6916 -12137
- package/src/components/editor/grid.tsx +197 -36
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +420 -0
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +396 -0
- package/src/components/editor/group-transform-shared.ts +489 -0
- package/src/components/editor/handles/handle-arrow.tsx +581 -0
- package/src/components/editor/handles/use-handle-drag.ts +246 -0
- package/src/components/editor/index.tsx +412 -162
- package/src/components/editor/measurement-pill.tsx +115 -0
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +1646 -0
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +1106 -483
- package/src/components/editor/site-edge-labels.tsx +45 -21
- package/src/components/editor/slab-hole-highlights.tsx +474 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +140 -41
- package/src/components/editor/use-floorplan-background-placement.ts +190 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +19 -7
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +14 -19
- package/src/components/editor/wall-move-side-handles.tsx +870 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
- package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +375 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
- package/src/components/tools/fence/fence-drafting.ts +141 -27
- package/src/components/tools/item/move-tool.tsx +45 -105
- package/src/components/tools/item/placement-math.test.ts +22 -0
- package/src/components/tools/item/placement-math.ts +59 -7
- package/src/components/tools/item/placement-strategies.ts +432 -16
- package/src/components/tools/item/placement-types.ts +22 -1
- package/src/components/tools/item/use-draft-node.ts +51 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
- package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
- package/src/components/tools/roof/roof-tool.tsx +350 -28
- package/src/components/tools/select/box-select-state.ts +88 -0
- package/src/components/tools/select/box-select-tool.tsx +442 -520
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +305 -0
- package/src/components/tools/shared/polygon-editor.tsx +866 -175
- package/src/components/tools/shared/segment-angle.ts +56 -0
- package/src/components/tools/site/site-boundary-editor.tsx +520 -16
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +2 -1
- package/src/components/tools/stair/stair-tool.tsx +342 -37
- package/src/components/tools/tool-manager.tsx +239 -62
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +248 -171
- package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +86 -82
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -170
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +3 -117
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +27 -93
- package/src/components/ui/action-menu/view-toggles.tsx +279 -90
- package/src/components/ui/command-palette/editor-commands.tsx +20 -15
- package/src/components/ui/command-palette/index.tsx +5 -3
- package/src/components/ui/controls/material-paint-panel.tsx +152 -0
- package/src/components/ui/controls/material-picker.tsx +87 -177
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +142 -46
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +235 -23
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +17 -16
- package/src/components/ui/panels/panel-manager.tsx +55 -58
- package/src/components/ui/panels/panel-wrapper.tsx +238 -12
- package/src/components/ui/panels/parametric-inspector.tsx +482 -0
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +75 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +253 -61
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +64 -10
- package/src/hooks/use-ceiling-events.ts +176 -0
- package/src/hooks/use-drag-action.ts +127 -0
- package/src/hooks/use-keyboard.ts +493 -95
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +451 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/constants.ts +6 -2
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +50 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +117 -0
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +10 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +339 -0
- package/src/lib/fresh-planar-placement.ts +130 -0
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +11 -1
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-duplication.ts +54 -42
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +227 -48
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +100 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +7 -7
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +332 -0
- package/src/lib/scene.ts +40 -8
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +69 -10
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +148 -55
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +231 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +767 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +39 -0
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +34 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -280
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Ephemeral store for the forward-facing floor triangle shown while placing or
|
|
2
|
+
// moving a node. A single editor-side overlay (`<FacingPoseIndicator>`)
|
|
3
|
+
// subscribes and renders the triangle; every placement/move path publishes its
|
|
4
|
+
// ghost pose here instead of drawing its own triangle. This is deliberately the
|
|
5
|
+
// one renderer for the facing indicator: rendering it from inside a tool's own
|
|
6
|
+
// cursor ghost (especially tools living in `@pascal-app/nodes`) left it
|
|
7
|
+
// invisible, while the editor-side overlay renders reliably. Producers clear on
|
|
8
|
+
// commit, cancel, and unmount.
|
|
9
|
+
//
|
|
10
|
+
// Poses are in the same building-local frame the tools already work in (the
|
|
11
|
+
// overlay is mounted inside ToolManager's building-local group).
|
|
12
|
+
|
|
13
|
+
import { create } from 'zustand'
|
|
14
|
+
|
|
15
|
+
export type FacingPose = {
|
|
16
|
+
/** Ghost origin in building-local space. */
|
|
17
|
+
position: [number, number, number]
|
|
18
|
+
/** Ghost yaw (radians). The triangle inherits this so it points where the
|
|
19
|
+
* node faces. */
|
|
20
|
+
rotationY: number
|
|
21
|
+
/** Footprint depth along the ghost's local +Z; the triangle sits just past
|
|
22
|
+
* `center[1] + depth / 2`. */
|
|
23
|
+
depth: number
|
|
24
|
+
/** Footprint centre offset `[x, z]` in the ghost's local frame. Defaults to
|
|
25
|
+
* the origin. Kinds whose forward edge isn't centred on the origin (e.g. a
|
|
26
|
+
* stair, whose run starts at the entry) shift the triangle via this. */
|
|
27
|
+
center?: [number, number]
|
|
28
|
+
/** Point along local -Z (the front is the -Z side) instead of +Z. */
|
|
29
|
+
reversed?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type FacingPoseState = {
|
|
33
|
+
pose: FacingPose | null
|
|
34
|
+
set(pose: FacingPose): void
|
|
35
|
+
clear(): void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const useFacingPose = create<FacingPoseState>((set) => ({
|
|
39
|
+
pose: null,
|
|
40
|
+
set: (pose) => set({ pose }),
|
|
41
|
+
clear: () => set({ pose: null }),
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
export default useFacingPose
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Ephemeral store: how many points the in-progress curved-fence draft has
|
|
2
|
+
// placed. Written by the 3D spline draft tool (`@pascal-app/nodes`
|
|
3
|
+
// fence/tool.tsx) and read by the contextual helper so the "finish curve" hint
|
|
4
|
+
// only surfaces once the user has actually started drawing. Reset on commit,
|
|
5
|
+
// cancel, and unmount — never persisted, never in undo history.
|
|
6
|
+
|
|
7
|
+
import { create } from 'zustand'
|
|
8
|
+
|
|
9
|
+
type FenceCurveDraftState = {
|
|
10
|
+
pointCount: number
|
|
11
|
+
setPointCount(count: number): void
|
|
12
|
+
reset(): void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const useFenceCurveDraft = create<FenceCurveDraftState>((set) => ({
|
|
16
|
+
pointCount: 0,
|
|
17
|
+
setPointCount: (count) => set({ pointCount: count }),
|
|
18
|
+
reset: () => set({ pointCount: 0 }),
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
export default useFenceCurveDraft
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Ephemeral store for the 2D floor-plan's in-flight DRAFT preview state — the
|
|
2
|
+
// hot, per-pointer-move values every build/edit tool republishes on `grid:move`
|
|
3
|
+
// (the snapped cursor point today; wall/fence/roof draft endpoints as later
|
|
4
|
+
// slices land here). It exists so those per-move updates DON'T live in
|
|
5
|
+
// `FloorplanPanel`'s own `useState`: the panel is a ~10k-line component whose
|
|
6
|
+
// render costs ~120-220ms, so a `setState` per move made every 2D draft tool
|
|
7
|
+
// feel laggy. Producers write via `getState().setX(...)` (no panel re-render);
|
|
8
|
+
// the small overlay leaves subscribe and re-render alone. Same pattern that
|
|
9
|
+
// keeps stair / column / elevator placement smooth (`useStairBuildPreview`,
|
|
10
|
+
// `usePlacementPreview`).
|
|
11
|
+
//
|
|
12
|
+
// Editor-only. Producers clear on tool-inactive, commit, and unmount.
|
|
13
|
+
|
|
14
|
+
import type { WallPlanPoint } from '@pascal-app/core'
|
|
15
|
+
import { create } from 'zustand'
|
|
16
|
+
|
|
17
|
+
/** Screen-space (SVG-local px) cursor point — drives the coordinate badge. */
|
|
18
|
+
type SvgPoint = { x: number; y: number }
|
|
19
|
+
|
|
20
|
+
type FloorplanDraftPreviewState = {
|
|
21
|
+
/** Snapped plan-XZ point under the cursor; drives the crosshair + the
|
|
22
|
+
* cursor-following polygon-draft preview. `null` when idle. */
|
|
23
|
+
cursorPoint: WallPlanPoint | null
|
|
24
|
+
/** Screen-space cursor point driving the coordinate-indicator badge. Set on
|
|
25
|
+
* every SVG `pointermove` while a build/select tool is active, so it's the
|
|
26
|
+
* single hottest 2D update — keeping it out of panel state is what stops the
|
|
27
|
+
* panel re-rendering per move. `null` when idle. */
|
|
28
|
+
cursorPosition: SvgPoint | null
|
|
29
|
+
/** Live END point of the open wall / fence / roof draft segment — the per-move
|
|
30
|
+
* endpoint that drives the 2D draft polygon + measurement. Each is `null`
|
|
31
|
+
* unless that tool's draft is open. The START points stay in panel state
|
|
32
|
+
* (set per click, low-frequency). */
|
|
33
|
+
wallDraftEnd: WallPlanPoint | null
|
|
34
|
+
fenceDraftEnd: WallPlanPoint | null
|
|
35
|
+
roofDraftEnd: WallPlanPoint | null
|
|
36
|
+
/** Set the snapped cursor point. No-ops (skips the store update, so
|
|
37
|
+
* subscribers don't re-render) when unchanged — `grid:move` fires far more
|
|
38
|
+
* often than the snapped cell actually changes. */
|
|
39
|
+
setCursorPoint(point: WallPlanPoint | null): void
|
|
40
|
+
/** Set the screen-space cursor point (deduped on x/y). */
|
|
41
|
+
setCursorPosition(point: SvgPoint | null): void
|
|
42
|
+
setWallDraftEnd(point: WallPlanPoint | null): void
|
|
43
|
+
setFenceDraftEnd(point: WallPlanPoint | null): void
|
|
44
|
+
setRoofDraftEnd(point: WallPlanPoint | null): void
|
|
45
|
+
reset(): void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function setPlanPointField(
|
|
49
|
+
field: 'wallDraftEnd' | 'fenceDraftEnd' | 'roofDraftEnd',
|
|
50
|
+
point: WallPlanPoint | null,
|
|
51
|
+
) {
|
|
52
|
+
return (
|
|
53
|
+
state: FloorplanDraftPreviewState,
|
|
54
|
+
): Partial<FloorplanDraftPreviewState> | typeof state => {
|
|
55
|
+
const prev = state[field]
|
|
56
|
+
if (!point && !prev) return state
|
|
57
|
+
if (point && prev && prev[0] === point[0] && prev[1] === point[1]) return state
|
|
58
|
+
return { [field]: point }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set) => ({
|
|
63
|
+
cursorPoint: null,
|
|
64
|
+
cursorPosition: null,
|
|
65
|
+
wallDraftEnd: null,
|
|
66
|
+
fenceDraftEnd: null,
|
|
67
|
+
roofDraftEnd: null,
|
|
68
|
+
setCursorPoint: (point) =>
|
|
69
|
+
set((state) => {
|
|
70
|
+
const prev = state.cursorPoint
|
|
71
|
+
if (!point && !prev) return state
|
|
72
|
+
if (point && prev && prev[0] === point[0] && prev[1] === point[1]) return state
|
|
73
|
+
return { cursorPoint: point }
|
|
74
|
+
}),
|
|
75
|
+
setCursorPosition: (point) =>
|
|
76
|
+
set((state) => {
|
|
77
|
+
const prev = state.cursorPosition
|
|
78
|
+
if (!point && !prev) return state
|
|
79
|
+
if (point && prev && prev.x === point.x && prev.y === point.y) return state
|
|
80
|
+
return { cursorPosition: point }
|
|
81
|
+
}),
|
|
82
|
+
setWallDraftEnd: (point) => set(setPlanPointField('wallDraftEnd', point)),
|
|
83
|
+
setFenceDraftEnd: (point) => set(setPlanPointField('fenceDraftEnd', point)),
|
|
84
|
+
setRoofDraftEnd: (point) => set(setPlanPointField('roofDraftEnd', point)),
|
|
85
|
+
reset: () =>
|
|
86
|
+
set((state) =>
|
|
87
|
+
state.cursorPoint === null &&
|
|
88
|
+
state.cursorPosition === null &&
|
|
89
|
+
state.wallDraftEnd === null &&
|
|
90
|
+
state.fenceDraftEnd === null &&
|
|
91
|
+
state.roofDraftEnd === null
|
|
92
|
+
? state
|
|
93
|
+
: {
|
|
94
|
+
cursorPoint: null,
|
|
95
|
+
cursorPosition: null,
|
|
96
|
+
wallDraftEnd: null,
|
|
97
|
+
fenceDraftEnd: null,
|
|
98
|
+
roofDraftEnd: null,
|
|
99
|
+
},
|
|
100
|
+
),
|
|
101
|
+
}))
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Ephemeral store for the 2D floor-plan marquee (box-select) drag — the hot,
|
|
2
|
+
// per-pointer-move rectangle the select tool republishes on every move. It
|
|
3
|
+
// lives here, not in `FloorplanPanel`'s `useState`, for the same reason as
|
|
4
|
+
// `useFloorplanDraftPreview`: the panel is a ~10k-line component whose render
|
|
5
|
+
// costs ~120-220ms, so a `setState` per move made dragging a selection box
|
|
6
|
+
// re-render the whole panel. Producers write via `getState()` (no panel
|
|
7
|
+
// re-render); the small marquee overlay leaf subscribes and re-renders alone.
|
|
8
|
+
//
|
|
9
|
+
// The whole drag struct lives here (not just the moving corner) so the
|
|
10
|
+
// pointer-move / -up handlers read it non-reactively via `getState()` and the
|
|
11
|
+
// panel never subscribes. Editor-only; reset on pointer-up / cancel /
|
|
12
|
+
// tool-inactive.
|
|
13
|
+
|
|
14
|
+
import type { WallPlanPoint } from '@pascal-app/core'
|
|
15
|
+
import { create } from 'zustand'
|
|
16
|
+
|
|
17
|
+
export type FloorplanMarqueeDrag = {
|
|
18
|
+
pointerId: number
|
|
19
|
+
startClientX: number
|
|
20
|
+
startClientY: number
|
|
21
|
+
startPlanPoint: WallPlanPoint
|
|
22
|
+
/** Moving corner under the cursor — the only field that changes per move. */
|
|
23
|
+
currentPlanPoint: WallPlanPoint
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type FloorplanMarqueeState = {
|
|
27
|
+
drag: FloorplanMarqueeDrag | null
|
|
28
|
+
begin(drag: FloorplanMarqueeDrag): void
|
|
29
|
+
/** Advance the moving corner. No-ops (skips the store update, so the overlay
|
|
30
|
+
* doesn't re-render) when the snapped point is unchanged or no drag is open. */
|
|
31
|
+
setCurrent(point: WallPlanPoint): void
|
|
32
|
+
reset(): void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const useFloorplanMarquee = create<FloorplanMarqueeState>((set) => ({
|
|
36
|
+
drag: null,
|
|
37
|
+
begin: (drag) => set({ drag }),
|
|
38
|
+
setCurrent: (point) =>
|
|
39
|
+
set((state) => {
|
|
40
|
+
const prev = state.drag
|
|
41
|
+
if (!prev) return state
|
|
42
|
+
if (prev.currentPlanPoint[0] === point[0] && prev.currentPlanPoint[1] === point[1]) {
|
|
43
|
+
return state
|
|
44
|
+
}
|
|
45
|
+
return { drag: { ...prev, currentPlanPoint: point } }
|
|
46
|
+
}),
|
|
47
|
+
reset: () => set((state) => (state.drag === null ? state : { drag: null })),
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
export default useFloorplanMarquee
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { AnyNode } from '@pascal-app/core'
|
|
3
|
+
import {
|
|
4
|
+
type ActiveInteractionScope,
|
|
5
|
+
editingHoleInfo,
|
|
6
|
+
handleDragInfo,
|
|
7
|
+
isActive,
|
|
8
|
+
isIdle,
|
|
9
|
+
scopeNodeId,
|
|
10
|
+
selectionEnabled,
|
|
11
|
+
} from '../lib/interaction/scope'
|
|
12
|
+
import useInteractionScope from './use-interaction-scope'
|
|
13
|
+
|
|
14
|
+
// A placing/moving scope carries the node inline. Tests only assert on id/type,
|
|
15
|
+
// so a structural stand-in is enough.
|
|
16
|
+
const mockNode = (id: string, type: string): AnyNode => ({ id, type }) as unknown as AnyNode
|
|
17
|
+
|
|
18
|
+
function reset() {
|
|
19
|
+
useInteractionScope.getState().end()
|
|
20
|
+
}
|
|
21
|
+
afterEach(reset)
|
|
22
|
+
|
|
23
|
+
describe('use-interaction-scope state machine', () => {
|
|
24
|
+
test('starts idle', () => {
|
|
25
|
+
expect(useInteractionScope.getState().scope.kind).toBe('idle')
|
|
26
|
+
expect(isIdle(useInteractionScope.getState().scope)).toBe(true)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('begin enters an interaction; end returns to idle atomically', () => {
|
|
30
|
+
const s = useInteractionScope.getState()
|
|
31
|
+
s.begin({
|
|
32
|
+
kind: 'moving',
|
|
33
|
+
node: mockNode('item_1', 'item'),
|
|
34
|
+
nodeId: 'item_1',
|
|
35
|
+
nodeType: 'item',
|
|
36
|
+
view: '3d',
|
|
37
|
+
})
|
|
38
|
+
expect(useInteractionScope.getState().scope).toEqual({
|
|
39
|
+
kind: 'moving',
|
|
40
|
+
node: mockNode('item_1', 'item'),
|
|
41
|
+
nodeId: 'item_1',
|
|
42
|
+
nodeType: 'item',
|
|
43
|
+
view: '3d',
|
|
44
|
+
})
|
|
45
|
+
s.end()
|
|
46
|
+
// No interaction payload leaks past end — the scope is plain idle, so a
|
|
47
|
+
// stale nodeId/handle is unrepresentable.
|
|
48
|
+
expect(useInteractionScope.getState().scope).toEqual({ kind: 'idle' })
|
|
49
|
+
expect(scopeNodeId(useInteractionScope.getState().scope)).toBeNull()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('begin is single-owner: a new interaction replaces the prior one', () => {
|
|
53
|
+
const s = useInteractionScope.getState()
|
|
54
|
+
s.begin({ kind: 'drafting', tool: 'wall' })
|
|
55
|
+
s.begin({ kind: 'handle-drag', nodeId: 'wall_1', handle: 'height' })
|
|
56
|
+
const scope = useInteractionScope.getState().scope
|
|
57
|
+
expect(scope.kind).toBe('handle-drag')
|
|
58
|
+
// The prior drafting payload is gone — illegal "drafting + handle-drag"
|
|
59
|
+
// combination is unrepresentable.
|
|
60
|
+
expect(scopeNodeId(scope)).toBe('wall_1')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('update patches the live payload of the active scope', () => {
|
|
64
|
+
const s = useInteractionScope.getState()
|
|
65
|
+
s.begin({
|
|
66
|
+
kind: 'placing',
|
|
67
|
+
node: mockNode('i1', 'item'),
|
|
68
|
+
nodeId: 'i1',
|
|
69
|
+
nodeType: 'item',
|
|
70
|
+
view: '3d',
|
|
71
|
+
pressDrag: false,
|
|
72
|
+
})
|
|
73
|
+
s.update({ pressDrag: true })
|
|
74
|
+
const scope = useInteractionScope.getState().scope
|
|
75
|
+
expect(scope.kind === 'placing' && scope.pressDrag).toBe(true)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('update is a no-op when idle', () => {
|
|
79
|
+
useInteractionScope.getState().update({
|
|
80
|
+
kind: 'moving',
|
|
81
|
+
node: mockNode('x', 'item'),
|
|
82
|
+
nodeId: 'x',
|
|
83
|
+
nodeType: 'item',
|
|
84
|
+
view: '3d',
|
|
85
|
+
})
|
|
86
|
+
expect(useInteractionScope.getState().scope.kind).toBe('idle')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('update cannot change which interaction is running', () => {
|
|
90
|
+
const s = useInteractionScope.getState()
|
|
91
|
+
s.begin({
|
|
92
|
+
kind: 'moving',
|
|
93
|
+
node: mockNode('i1', 'item'),
|
|
94
|
+
nodeId: 'i1',
|
|
95
|
+
nodeType: 'item',
|
|
96
|
+
view: '3d',
|
|
97
|
+
})
|
|
98
|
+
s.update({
|
|
99
|
+
kind: 'placing',
|
|
100
|
+
node: mockNode('i1', 'item'),
|
|
101
|
+
nodeId: 'i1',
|
|
102
|
+
nodeType: 'item',
|
|
103
|
+
view: '3d',
|
|
104
|
+
pressDrag: true,
|
|
105
|
+
})
|
|
106
|
+
expect(useInteractionScope.getState().scope.kind).toBe('moving')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('selectionEnabled only while idle', () => {
|
|
110
|
+
const s = useInteractionScope.getState()
|
|
111
|
+
expect(selectionEnabled(useInteractionScope.getState().scope)).toBe(true)
|
|
112
|
+
s.begin({ kind: 'box-select' })
|
|
113
|
+
expect(selectionEnabled(useInteractionScope.getState().scope)).toBe(false)
|
|
114
|
+
expect(isActive(useInteractionScope.getState().scope)).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('end is idempotent', () => {
|
|
118
|
+
const s = useInteractionScope.getState()
|
|
119
|
+
s.end()
|
|
120
|
+
s.end()
|
|
121
|
+
expect(useInteractionScope.getState().scope.kind).toBe('idle')
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('derived flag views are leak-free (no parallel flags)', () => {
|
|
126
|
+
const scope = () => useInteractionScope.getState().scope
|
|
127
|
+
|
|
128
|
+
test('handleDragInfo mirrors handle-drag and clears on end', () => {
|
|
129
|
+
const s = useInteractionScope.getState()
|
|
130
|
+
s.begin({ kind: 'handle-drag', nodeId: 'wall_1', handle: 'height' })
|
|
131
|
+
expect(handleDragInfo(scope())).toEqual({ nodeId: 'wall_1', label: 'height' })
|
|
132
|
+
s.end()
|
|
133
|
+
// After end the derived view is null — a stale activeHandleDrag is
|
|
134
|
+
// unrepresentable because it is a pure function of the single scope.
|
|
135
|
+
expect(handleDragInfo(scope())).toBeNull()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('editingHoleInfo mirrors a hole reshape and clears on end', () => {
|
|
139
|
+
const s = useInteractionScope.getState()
|
|
140
|
+
s.begin({ kind: 'reshaping', nodeId: 'slab_1', reshape: 'hole', holeIndex: 2 })
|
|
141
|
+
expect(editingHoleInfo(scope())).toEqual({ nodeId: 'slab_1', holeIndex: 2 })
|
|
142
|
+
s.end()
|
|
143
|
+
expect(editingHoleInfo(scope())).toBeNull()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('a non-hole reshape never reads as an editing hole', () => {
|
|
147
|
+
const s = useInteractionScope.getState()
|
|
148
|
+
s.begin({ kind: 'reshaping', nodeId: 'wall_1', reshape: 'curve' })
|
|
149
|
+
expect(editingHoleInfo(scope())).toBeNull()
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('switching interactions never leaks the prior derived view', () => {
|
|
153
|
+
const s = useInteractionScope.getState()
|
|
154
|
+
s.begin({ kind: 'handle-drag', nodeId: 'wall_1', handle: 'height' })
|
|
155
|
+
// Single-owner replacement: the handle-drag view must vanish the instant a
|
|
156
|
+
// different interaction begins — the two cannot be simultaneously active.
|
|
157
|
+
s.begin({ kind: 'reshaping', nodeId: 'slab_1', reshape: 'hole', holeIndex: 0 })
|
|
158
|
+
expect(handleDragInfo(scope())).toBeNull()
|
|
159
|
+
expect(editingHoleInfo(scope())).toEqual({ nodeId: 'slab_1', holeIndex: 0 })
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('every active scope kind leaves at most the views it owns', () => {
|
|
163
|
+
const s = useInteractionScope.getState()
|
|
164
|
+
const kinds: ActiveInteractionScope[] = [
|
|
165
|
+
{
|
|
166
|
+
kind: 'placing',
|
|
167
|
+
node: mockNode('i', 'item'),
|
|
168
|
+
nodeId: 'i',
|
|
169
|
+
nodeType: 'item',
|
|
170
|
+
view: '3d',
|
|
171
|
+
pressDrag: false,
|
|
172
|
+
},
|
|
173
|
+
{ kind: 'moving', node: mockNode('i', 'item'), nodeId: 'i', nodeType: 'item', view: '3d' },
|
|
174
|
+
{ kind: 'drafting', tool: 'wall' },
|
|
175
|
+
{ kind: 'box-select' },
|
|
176
|
+
{ kind: 'painting' },
|
|
177
|
+
]
|
|
178
|
+
for (const k of kinds) {
|
|
179
|
+
s.begin(k)
|
|
180
|
+
// None of these own a handle-drag or hole view.
|
|
181
|
+
expect(handleDragInfo(scope())).toBeNull()
|
|
182
|
+
expect(editingHoleInfo(scope())).toBeNull()
|
|
183
|
+
}
|
|
184
|
+
s.end()
|
|
185
|
+
})
|
|
186
|
+
})
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type AnyNode, type AnyNodeId, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useRef } from 'react'
|
|
5
|
+
import { create } from 'zustand'
|
|
6
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
7
|
+
import {
|
|
8
|
+
type ActiveInteractionScope,
|
|
9
|
+
controlPointReshapeInfo,
|
|
10
|
+
editingHoleInfo,
|
|
11
|
+
endpointReshapeInfo,
|
|
12
|
+
handleDragInfo,
|
|
13
|
+
IDLE_SCOPE,
|
|
14
|
+
type InteractionScope,
|
|
15
|
+
isCurveReshape,
|
|
16
|
+
movingNodeOf,
|
|
17
|
+
reshapingNodeId,
|
|
18
|
+
tangentReshapeInfo,
|
|
19
|
+
} from '../lib/interaction/scope'
|
|
20
|
+
|
|
21
|
+
// The authoritative interaction state machine. A single owner holds exactly one
|
|
22
|
+
// scope at a time. `begin` enters an interaction (atomically replacing any prior
|
|
23
|
+
// one — a single owner, no producer races), `update` narrows the live payload,
|
|
24
|
+
// and `end` returns to idle atomically so no interaction payload can leak past
|
|
25
|
+
// the end of its interaction. There is no setter that can leave the store in an
|
|
26
|
+
// illegal half-state: the only writable shape is `InteractionScope`.
|
|
27
|
+
|
|
28
|
+
export type InteractionScopeState = {
|
|
29
|
+
scope: InteractionScope
|
|
30
|
+
// Enter an interaction. If one is already active it is ended first, so the
|
|
31
|
+
// store is always single-owner.
|
|
32
|
+
begin: (scope: ActiveInteractionScope) => void
|
|
33
|
+
// Patch the current scope's payload. Ignored when idle, or when the patch's
|
|
34
|
+
// implied kind differs from the active kind — payload updates must not change
|
|
35
|
+
// which interaction is running (use `begin` for that).
|
|
36
|
+
update: (patch: Partial<ActiveInteractionScope>) => void
|
|
37
|
+
// Return to idle atomically. Both commit and cancel paths call this; the
|
|
38
|
+
// distinction (write vs revert) lives in the interaction body, not here.
|
|
39
|
+
end: () => void
|
|
40
|
+
// Return to idle only if the active scope matches `match`. Used when scope is
|
|
41
|
+
// driven from independent legacy flag clears, so clearing one flag (e.g. a
|
|
42
|
+
// fence curve) cannot stomp an unrelated active scope (e.g. a wall move).
|
|
43
|
+
endIf: (match: (scope: ActiveInteractionScope) => boolean) => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const useInteractionScope = create<InteractionScopeState>((set, get) => ({
|
|
47
|
+
scope: IDLE_SCOPE,
|
|
48
|
+
begin: (scope) => set({ scope }),
|
|
49
|
+
update: (patch) =>
|
|
50
|
+
set((state) => {
|
|
51
|
+
if (state.scope.kind === 'idle') return state
|
|
52
|
+
if ('kind' in patch && patch.kind !== state.scope.kind) return state
|
|
53
|
+
return { scope: { ...state.scope, ...patch } as InteractionScope }
|
|
54
|
+
}),
|
|
55
|
+
end: () => {
|
|
56
|
+
if (get().scope.kind === 'idle') return
|
|
57
|
+
set({ scope: IDLE_SCOPE })
|
|
58
|
+
},
|
|
59
|
+
endIf: (match) => {
|
|
60
|
+
const scope = get().scope
|
|
61
|
+
if (scope.kind === 'idle') return
|
|
62
|
+
if (match(scope)) set({ scope: IDLE_SCOPE })
|
|
63
|
+
},
|
|
64
|
+
}))
|
|
65
|
+
|
|
66
|
+
// Derived, reference-stable views of the active scope, replacing the legacy
|
|
67
|
+
// `useEditor.activeHandleDrag` / `useEditor.editingHole` flags. `useShallow`
|
|
68
|
+
// keeps the result reference-stable across unrelated scope changes, so hot-path
|
|
69
|
+
// subscribers (camera controls, floating menu) don't re-render on every update.
|
|
70
|
+
export const useActiveHandleDrag = (): { nodeId: string; label: string } | null =>
|
|
71
|
+
useInteractionScope(useShallow((s) => handleDragInfo(s.scope)))
|
|
72
|
+
|
|
73
|
+
export const useEditingHole = (): { nodeId: string; holeIndex: number } | null =>
|
|
74
|
+
useInteractionScope(useShallow((s) => editingHoleInfo(s.scope)))
|
|
75
|
+
|
|
76
|
+
// Imperative (non-React) reads for event handlers / effects.
|
|
77
|
+
export const getEditingHole = (): { nodeId: string; holeIndex: number } | null =>
|
|
78
|
+
editingHoleInfo(useInteractionScope.getState().scope)
|
|
79
|
+
|
|
80
|
+
export const getIsCurveReshape = (): boolean => isCurveReshape(useInteractionScope.getState().scope)
|
|
81
|
+
|
|
82
|
+
// Replaces the legacy `curvingWall` / `curvingFence` existence flags. The
|
|
83
|
+
// wall-vs-fence distinction (both now map to one `reshaping/'curve'` scope) is
|
|
84
|
+
// recovered by reading the reshaped node's type from `useReshapingNode`.
|
|
85
|
+
export const useIsCurveReshape = (): boolean => useInteractionScope((s) => isCurveReshape(s.scope))
|
|
86
|
+
|
|
87
|
+
// Replaces the legacy `movingWallEndpoint` / `movingFenceEndpoint` payloads,
|
|
88
|
+
// minus the node (fetch it from `useReshapingNode`).
|
|
89
|
+
export const useEndpointReshape = (): { nodeId: string; endpoint: 'start' | 'end' } | null =>
|
|
90
|
+
useInteractionScope(useShallow((s) => endpointReshapeInfo(s.scope)))
|
|
91
|
+
|
|
92
|
+
export const useControlPointReshape = (): { nodeId: string; index: number } | null =>
|
|
93
|
+
useInteractionScope(useShallow((s) => controlPointReshapeInfo(s.scope)))
|
|
94
|
+
|
|
95
|
+
export const useTangentReshape = (): { nodeId: string; index: number; side: 'in' | 'out' } | null =>
|
|
96
|
+
useInteractionScope(useShallow((s) => tangentReshapeInfo(s.scope)))
|
|
97
|
+
|
|
98
|
+
// The node currently being reshaped (curve / endpoint / hole), looked up live
|
|
99
|
+
// from the scene by the scope's `nodeId`. During a reshape the scene node holds
|
|
100
|
+
// the same data the legacy `curvingWall` / `movingWallEndpoint.wall` carried, so
|
|
101
|
+
// consumers that need the full node (affordance-tool mounts, wall-vs-fence type
|
|
102
|
+
// checks) read it here instead of from a parallel flag.
|
|
103
|
+
export const useReshapingNode = (): AnyNode | null => {
|
|
104
|
+
const nodeId = useInteractionScope((s) => reshapingNodeId(s.scope))
|
|
105
|
+
// Snapshot the node ONCE when the reshape begins (keyed on nodeId), like the
|
|
106
|
+
// legacy `curvingWall` / `movingWallEndpoint.wall` flags did. The affordance
|
|
107
|
+
// tools write the node live during the drag; subscribing to the live scene
|
|
108
|
+
// node would feed those writes straight back into the tool — the curve resets
|
|
109
|
+
// on pointer-stop, the endpoint drag loops and freezes. nodeId is stable for
|
|
110
|
+
// the whole gesture, so a ref snapshot stays frozen until the next reshape.
|
|
111
|
+
const snapshot = useRef<{ id: string | null; node: AnyNode | null }>({ id: null, node: null })
|
|
112
|
+
if (snapshot.current.id !== nodeId) {
|
|
113
|
+
snapshot.current = {
|
|
114
|
+
id: nodeId,
|
|
115
|
+
node: nodeId ? (useScene.getState().nodes[nodeId as AnyNodeId] ?? null) : null,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return snapshot.current.node
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// The node currently being placed or moved. Replaces the legacy
|
|
122
|
+
// `useEditor.movingNode` flag. Unlike `useReshapingNode`, no `useRef` snapshot is
|
|
123
|
+
// needed: the node is carried inline in the scope and set once at `begin`, so it
|
|
124
|
+
// is already a stable reference for the whole gesture (nothing calls `begin` mid
|
|
125
|
+
// drag). Returns null whenever no placing/moving interaction is active.
|
|
126
|
+
export const useMovingNode = (): AnyNode | null => useInteractionScope((s) => movingNodeOf(s.scope))
|
|
127
|
+
|
|
128
|
+
// Imperative (non-React) read for event handlers / effects.
|
|
129
|
+
export const getMovingNode = (): AnyNode | null =>
|
|
130
|
+
movingNodeOf(useInteractionScope.getState().scope)
|
|
131
|
+
|
|
132
|
+
export default useInteractionScope
|