@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
|
@@ -1,11 +1,96 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
emitter,
|
|
5
|
+
nodeRegistry,
|
|
6
|
+
pauseSpaceDetection,
|
|
7
|
+
resumeSpaceDetection,
|
|
8
|
+
useScene,
|
|
9
|
+
} from '@pascal-app/core'
|
|
2
10
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
11
|
import { useEffect } from 'react'
|
|
4
|
-
import {
|
|
12
|
+
import { Vector3 } from 'three'
|
|
13
|
+
import {
|
|
14
|
+
classifyParticipant,
|
|
15
|
+
collectParticipants,
|
|
16
|
+
computeGroupBox,
|
|
17
|
+
expandToComponent,
|
|
18
|
+
levelFrame,
|
|
19
|
+
rotateGroupPatches,
|
|
20
|
+
} from '../components/editor/group-transform-shared'
|
|
21
|
+
import { steppedRotation } from '../components/tools/item/placement-math'
|
|
22
|
+
import { resolveDirectManipulationNode } from '../lib/direct-manipulation'
|
|
23
|
+
import { toggleDoorOpenState } from '../lib/door-interaction'
|
|
24
|
+
import { guideEmitter } from '../lib/guide-events'
|
|
5
25
|
import { runRedo, runUndo } from '../lib/history'
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
26
|
+
import { isActive } from '../lib/interaction/scope'
|
|
27
|
+
import {
|
|
28
|
+
copySelectedNodesToEditorClipboard,
|
|
29
|
+
pasteEditorClipboardToLevel,
|
|
30
|
+
} from '../lib/scene-clipboard'
|
|
31
|
+
import { emitDeleteSFX, sfxEmitter } from '../lib/sfx-bus'
|
|
32
|
+
import { toggleWindowOpenState } from '../lib/window-interaction'
|
|
33
|
+
import useEditor, { getActiveContinuationContext, getActiveSnapContext } from '../store/use-editor'
|
|
34
|
+
import useInteractionScope, { getMovingNode } from '../store/use-interaction-scope'
|
|
35
|
+
|
|
36
|
+
// References (guide/scan) are selected via `useEditor.selectedReferenceId`, not
|
|
37
|
+
// the viewer selection, so selection-based key arms (R/T rotate) need this
|
|
38
|
+
// separate lookup. Locked guides don't rotate, matching direct manipulation.
|
|
39
|
+
function getRotatableSelectedReference() {
|
|
40
|
+
const refId = useEditor.getState().selectedReferenceId
|
|
41
|
+
if (!refId) return null
|
|
42
|
+
const node = useScene.getState().nodes[refId as AnyNodeId]
|
|
43
|
+
if (!node || (node.type !== 'guide' && node.type !== 'scan')) return null
|
|
44
|
+
if (useEditor.getState().guideUi[refId]?.locked === true) return null
|
|
45
|
+
return node
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Group rotate: R/T on a multi-selection spins the whole selection rigidly
|
|
49
|
+
// ±45° around its bbox center — the keyboard sibling of the 3D group-rotate
|
|
50
|
+
// gizmo, sharing its participant snapshot + rigid-rotation math (welded
|
|
51
|
+
// wall/fence junctions, connected-component expansion). One batched
|
|
52
|
+
// `updateNodes` call = one undo step. Returns false when the selection holds
|
|
53
|
+
// no transformable participants so the caller can fall through to the
|
|
54
|
+
// single-selection arms.
|
|
55
|
+
function rotateGroupSelection(direction: 1 | -1): boolean {
|
|
56
|
+
const { selectedIds, levelId } = useViewer.getState().selection
|
|
57
|
+
if (selectedIds.length <= 1) return false
|
|
58
|
+
const nodes = useScene.getState().nodes
|
|
59
|
+
const participantIds = selectedIds.filter(
|
|
60
|
+
(id) => classifyParticipant(nodes[id as AnyNodeId], levelId, nodes) !== null,
|
|
61
|
+
)
|
|
62
|
+
if (participantIds.length === 0) return false
|
|
63
|
+
const fullIds = expandToComponent(participantIds, nodes, levelId)
|
|
64
|
+
const { starts, links } = collectParticipants(fullIds, nodes, levelId)
|
|
65
|
+
if (starts.length === 0) return false
|
|
66
|
+
|
|
67
|
+
// Same pivot as the 3D gizmo: the selection's world bbox center, converted
|
|
68
|
+
// into the level frame before orbiting placements (a rotated building would
|
|
69
|
+
// otherwise displace the centre).
|
|
70
|
+
const box = computeGroupBox(fullIds)
|
|
71
|
+
if (!box) return false
|
|
72
|
+
const worldCenter = new Vector3(
|
|
73
|
+
(box.min.x + box.max.x) / 2,
|
|
74
|
+
box.min.y,
|
|
75
|
+
(box.min.z + box.max.z) / 2,
|
|
76
|
+
)
|
|
77
|
+
const localCenter = worldCenter.applyMatrix4(levelFrame(levelId).inverse)
|
|
78
|
+
|
|
79
|
+
// R (+45° yaw) orbits by -45° in the atan2 x→z sense: yaw = rotation - delta
|
|
80
|
+
// (see rotateGroupPatches), so keyboard direction matches the single-node
|
|
81
|
+
// steppedRotation sense.
|
|
82
|
+
const delta = -direction * (Math.PI / 4)
|
|
83
|
+
const patches = rotateGroupPatches(starts, links, { x: localCenter.x, z: localCenter.z }, delta)
|
|
84
|
+
// Space detection stays out: a rigid rotation of existing walls must not
|
|
85
|
+
// re-create the room's auto floors/ceilings at the new bearing.
|
|
86
|
+
pauseSpaceDetection()
|
|
87
|
+
useScene
|
|
88
|
+
.getState()
|
|
89
|
+
.updateNodes(patches.map(([id, data]) => ({ id, data: data as Partial<AnyNode> })))
|
|
90
|
+
resumeSpaceDetection()
|
|
91
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
92
|
+
return true
|
|
93
|
+
}
|
|
9
94
|
|
|
10
95
|
// Tools call this in their onCancel handler when they have an active mid-action to cancel,
|
|
11
96
|
// so that the global Escape handler knows not to also switch to select mode.
|
|
@@ -14,6 +99,64 @@ export const markToolCancelConsumed = () => {
|
|
|
14
99
|
_toolCancelConsumed = true
|
|
15
100
|
}
|
|
16
101
|
|
|
102
|
+
// Escape's fall-through when no tool consumed the cancel: drop back to the
|
|
103
|
+
// select tool (keeping building/level context) and close panels. Tools like
|
|
104
|
+
// preset/item placement rely on this — they pass no coordinator onCancel, and
|
|
105
|
+
// it is the mode switch unmounting them that destroys the draft.
|
|
106
|
+
const exitToSelectAfterUnconsumedCancel = () => {
|
|
107
|
+
const currentPhase = useEditor.getState().phase
|
|
108
|
+
const currentStructureLayer = useEditor.getState().structureLayer
|
|
109
|
+
|
|
110
|
+
useInteractionScope.getState().endIf((sc) => sc.kind === 'reshaping' && sc.reshape === 'hole')
|
|
111
|
+
|
|
112
|
+
// From zone mode, return to structure select
|
|
113
|
+
if (currentPhase === 'structure' && currentStructureLayer === 'zones') {
|
|
114
|
+
useEditor.getState().setStructureLayer('elements')
|
|
115
|
+
useEditor.getState().setMode('select')
|
|
116
|
+
} else {
|
|
117
|
+
// Return to the default select tool while keeping the active building/level context.
|
|
118
|
+
useEditor.getState().setMode('select')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
useEditor.getState().setFloorplanSelectionTool('click')
|
|
122
|
+
|
|
123
|
+
// Clear selections to close UI panels, but KEEP the active building and level context.
|
|
124
|
+
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
125
|
+
useEditor.getState().setSelectedReferenceId(null)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ⌘Z pressed mid-interaction (moving a node, drawing a wall, mid-placement…)
|
|
129
|
+
// reads as "abort this action", not history undo — behave exactly like Escape
|
|
130
|
+
// and report whether anything was in flight so the undo/redo arms know to
|
|
131
|
+
// skip the history jump. Pointer drags that only listen for their own
|
|
132
|
+
// capture-phase keydown never reach here — they stopPropagation first (see
|
|
133
|
+
// isHistoryShortcut call sites).
|
|
134
|
+
const cancelInteractionForHistoryShortcut = () => {
|
|
135
|
+
if (useEditor.getState().referenceScaleActiveGuideId) {
|
|
136
|
+
guideEmitter.emit('guide:cancel-reference-scale')
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
139
|
+
_toolCancelConsumed = false
|
|
140
|
+
emitter.emit('tool:cancel')
|
|
141
|
+
if (_toolCancelConsumed) return true
|
|
142
|
+
if (
|
|
143
|
+
isActive(useInteractionScope.getState().scope) ||
|
|
144
|
+
useViewer.getState().inputDragging ||
|
|
145
|
+
// Paused history means a gesture session is live (draft placement, adopted
|
|
146
|
+
// move, …) even when no scope/drag flag is set — the preset/item draft
|
|
147
|
+
// cycle keeps temporal paused for the whole session, and a history jump
|
|
148
|
+
// against a paused store would land on a stale baseline anyway.
|
|
149
|
+
!useScene.temporal.getState().isTracking
|
|
150
|
+
) {
|
|
151
|
+
// A gesture is live but nothing consumed the cancel: finish it the way
|
|
152
|
+
// Escape does — the mode switch is what actually cancels tools that hook
|
|
153
|
+
// their teardown to unmount (preset/item placement).
|
|
154
|
+
exitToSelectAfterUnconsumedCancel()
|
|
155
|
+
return true
|
|
156
|
+
}
|
|
157
|
+
return false
|
|
158
|
+
}
|
|
159
|
+
|
|
17
160
|
export const useKeyboard = ({
|
|
18
161
|
isVersionPreviewMode = false,
|
|
19
162
|
disabled = false,
|
|
@@ -26,39 +169,130 @@ export const useKeyboard = ({
|
|
|
26
169
|
return
|
|
27
170
|
}
|
|
28
171
|
|
|
172
|
+
// True while a door/window is being placed: either a fresh clone is moving
|
|
173
|
+
// (preset / duplicate path) or a door/window build tool is armed. The
|
|
174
|
+
// placement tool owns R/T then (flip the draft before commit), so the
|
|
175
|
+
// global selection-based R/T handler must stand down to avoid double-firing.
|
|
176
|
+
const isPlacingOpening = () => {
|
|
177
|
+
const ed = useEditor.getState()
|
|
178
|
+
const moving = getMovingNode()
|
|
179
|
+
if (moving?.type === 'door' || moving?.type === 'window') return true
|
|
180
|
+
return ed.mode === 'build' && (ed.tool === 'door' || ed.tool === 'window')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Shift cycles the snapping mode (and a clean-tap Ctrl the grid step)
|
|
184
|
+
// whenever there's an active snapping context — i.e. exactly when the HUD
|
|
185
|
+
// shows a snapping chip. That single source covers wall/fence/item drafting,
|
|
186
|
+
// every node move (including wall-hosted items + door/window openings, which
|
|
187
|
+
// now declare `snapProfile`), and endpoint/polygon reshaping, so the keys
|
|
188
|
+
// never silently stop working. Force-place lives on Alt where a tool supports it.
|
|
189
|
+
const isSnappingCycleContext = () => getActiveSnapContext() != null
|
|
190
|
+
// A "clean tap" of Ctrl/Meta (pressed and released with NO other key in
|
|
191
|
+
// between) cycles the grid step — same context as the Shift snapping-mode
|
|
192
|
+
// cycle. `ctrlTapClean` starts true the moment Ctrl/Meta goes down alone
|
|
193
|
+
// and is cleared the instant any other key fires, so chords like Ctrl+Z /
|
|
194
|
+
// Ctrl+C never cycle.
|
|
195
|
+
let ctrlTapClean = false
|
|
196
|
+
|
|
29
197
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
198
|
+
if (e.key === 'Control' || e.key === 'Meta') {
|
|
199
|
+
// Only a fresh, modifier-free press starts a clean-tap candidate;
|
|
200
|
+
// ignore key-repeat and presses already part of a combo.
|
|
201
|
+
ctrlTapClean = !e.repeat && !e.shiftKey && !e.altKey
|
|
202
|
+
} else {
|
|
203
|
+
// Any non-modifier key (or a modifier combined with Ctrl/Meta) breaks
|
|
204
|
+
// the clean tap.
|
|
205
|
+
ctrlTapClean = false
|
|
206
|
+
}
|
|
207
|
+
|
|
30
208
|
// Don't handle shortcuts if user is typing in an input
|
|
31
209
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
32
210
|
return
|
|
33
211
|
}
|
|
34
212
|
|
|
213
|
+
if (e.key === 'Shift' && !e.repeat && useEditor.getState().mode === 'material-paint') {
|
|
214
|
+
// In paint mode Shift cycles the application scope (this surface →
|
|
215
|
+
// whole item / all matching / room) — the paint-mode analogue of the
|
|
216
|
+
// snapping-mode cycle below. The scope chip mirrors this key.
|
|
217
|
+
e.preventDefault()
|
|
218
|
+
useEditor.getState().cyclePaintScope()
|
|
219
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (e.key === 'Shift' && !e.repeat && isSnappingCycleContext()) {
|
|
224
|
+
// Cycle the global snapping mode (grid → lines → angles → off).
|
|
225
|
+
// `'off'` is the snap bypass now, so Shift no longer holds-to-bypass.
|
|
226
|
+
e.preventDefault()
|
|
227
|
+
useEditor.getState().cycleSnappingMode()
|
|
228
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (
|
|
233
|
+
(e.key === 't' || e.key === 'T') &&
|
|
234
|
+
!e.repeat &&
|
|
235
|
+
!e.metaKey &&
|
|
236
|
+
!e.ctrlKey &&
|
|
237
|
+
!e.shiftKey &&
|
|
238
|
+
!e.altKey
|
|
239
|
+
) {
|
|
240
|
+
const context = getActiveContinuationContext()
|
|
241
|
+
if (context === 'fence') {
|
|
242
|
+
e.preventDefault()
|
|
243
|
+
const current = useEditor.getState().getContinuation('fence')
|
|
244
|
+
useEditor
|
|
245
|
+
.getState()
|
|
246
|
+
.setContinuation('fence', current === 'curved' ? 'continuous' : 'curved')
|
|
247
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (
|
|
253
|
+
(e.key === 'c' || e.key === 'C') &&
|
|
254
|
+
!e.repeat &&
|
|
255
|
+
!e.metaKey &&
|
|
256
|
+
!e.ctrlKey &&
|
|
257
|
+
!e.shiftKey &&
|
|
258
|
+
!e.altKey
|
|
259
|
+
) {
|
|
260
|
+
const context = getActiveContinuationContext()
|
|
261
|
+
if (context) {
|
|
262
|
+
e.preventDefault()
|
|
263
|
+
if (context === 'fence') {
|
|
264
|
+
const current = useEditor.getState().getContinuation('fence')
|
|
265
|
+
if (current !== 'curved') {
|
|
266
|
+
useEditor
|
|
267
|
+
.getState()
|
|
268
|
+
.setContinuation('fence', current === 'single' ? 'continuous' : 'single')
|
|
269
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
270
|
+
}
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
useEditor.getState().cycleContinuation(context)
|
|
274
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
35
279
|
if (e.key === 'Escape') {
|
|
36
280
|
e.preventDefault()
|
|
281
|
+
|
|
282
|
+
// An in-flight reference-scale measurement swallows Escape whole:
|
|
283
|
+
// cancel the flow but keep the reference selected and its panel open.
|
|
284
|
+
if (useEditor.getState().referenceScaleActiveGuideId) {
|
|
285
|
+
guideEmitter.emit('guide:cancel-reference-scale')
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
37
289
|
_toolCancelConsumed = false
|
|
38
290
|
emitter.emit('tool:cancel')
|
|
39
291
|
|
|
40
292
|
// Only switch to select mode if no tool had an active mid-action to cancel.
|
|
41
293
|
// (e.g. mid-wall draw or mid-slab polygon should only cancel the action, not exit the tool)
|
|
42
294
|
if (!_toolCancelConsumed) {
|
|
43
|
-
|
|
44
|
-
const currentStructureLayer = useEditor.getState().structureLayer
|
|
45
|
-
|
|
46
|
-
useEditor.getState().setEditingHole(null)
|
|
47
|
-
|
|
48
|
-
// From zone mode, return to structure select
|
|
49
|
-
if (currentPhase === 'structure' && currentStructureLayer === 'zones') {
|
|
50
|
-
useEditor.getState().setStructureLayer('elements')
|
|
51
|
-
useEditor.getState().setMode('select')
|
|
52
|
-
} else {
|
|
53
|
-
// Return to the default select tool while keeping the active building/level context.
|
|
54
|
-
useEditor.getState().setMode('select')
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
useEditor.getState().setFloorplanSelectionTool('click')
|
|
58
|
-
|
|
59
|
-
// Clear selections to close UI panels, but KEEP the active building and level context.
|
|
60
|
-
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
61
|
-
useEditor.getState().setSelectedReferenceId(null)
|
|
295
|
+
exitToSelectAfterUnconsumedCancel()
|
|
62
296
|
}
|
|
63
297
|
} else if (e.key === '1' && !e.metaKey && !e.ctrlKey) {
|
|
64
298
|
e.preventDefault()
|
|
@@ -77,6 +311,9 @@ export const useKeyboard = ({
|
|
|
77
311
|
e.preventDefault()
|
|
78
312
|
useEditor.getState().setPhase('furnish')
|
|
79
313
|
useEditor.getState().setMode('build')
|
|
314
|
+
// Set the item tool explicitly so the active tool never inherits a
|
|
315
|
+
// stale tool from a prior build session.
|
|
316
|
+
useEditor.getState().setTool('item')
|
|
80
317
|
useEditor.getState().setActiveSidebarPanel('items')
|
|
81
318
|
} else if (e.key === 'z' && !e.metaKey && !e.ctrlKey) {
|
|
82
319
|
if (isVersionPreviewMode) return
|
|
@@ -84,6 +321,17 @@ export const useKeyboard = ({
|
|
|
84
321
|
useEditor.getState().setPhase('structure')
|
|
85
322
|
useEditor.getState().setStructureLayer('zones')
|
|
86
323
|
useEditor.getState().setMode('build')
|
|
324
|
+
// Set the zone tool explicitly so it never inherits a stale tool.
|
|
325
|
+
useEditor.getState().setTool('zone')
|
|
326
|
+
} else if (e.key === 'm' && !e.metaKey && !e.ctrlKey) {
|
|
327
|
+
if (isVersionPreviewMode) return
|
|
328
|
+
e.preventDefault()
|
|
329
|
+
const editor = useEditor.getState()
|
|
330
|
+
editor.setPhase('structure')
|
|
331
|
+
editor.setStructureLayer('elements')
|
|
332
|
+
editor.setToolDefaults('measurement', { kind: editor.lastMeasurementKind })
|
|
333
|
+
editor.setMode('build')
|
|
334
|
+
editor.setTool('measurement')
|
|
87
335
|
}
|
|
88
336
|
if (e.key === 'v' && !e.metaKey && !e.ctrlKey) {
|
|
89
337
|
e.preventDefault()
|
|
@@ -95,7 +343,10 @@ export const useKeyboard = ({
|
|
|
95
343
|
useEditor.getState().setPhase('structure')
|
|
96
344
|
useEditor.getState().setStructureLayer('elements')
|
|
97
345
|
useEditor.getState().setMode('build')
|
|
98
|
-
|
|
346
|
+
// Set the wall tool explicitly so B never inherits a stale tool
|
|
347
|
+
// (e.g. fence) left over from a prior build session.
|
|
348
|
+
useEditor.getState().setTool('wall')
|
|
349
|
+
} else if (e.key === 'x' && !e.metaKey && !e.ctrlKey) {
|
|
99
350
|
if (isVersionPreviewMode) return
|
|
100
351
|
e.preventDefault()
|
|
101
352
|
useEditor.getState().setMode('delete')
|
|
@@ -106,26 +357,45 @@ export const useKeyboard = ({
|
|
|
106
357
|
useEditor.getState().setPhase('structure')
|
|
107
358
|
useEditor.getState().setStructureLayer('elements')
|
|
108
359
|
useEditor.getState().setMode('material-paint')
|
|
109
|
-
} else if (e.key === '
|
|
360
|
+
} else if (e.key === 'c' && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
|
|
110
361
|
if (isVersionPreviewMode) return
|
|
111
362
|
e.preventDefault()
|
|
112
|
-
|
|
113
|
-
} else if (e.key === '
|
|
363
|
+
copySelectedNodesToEditorClipboard()
|
|
364
|
+
} else if (e.key === 'v' && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
|
|
114
365
|
if (isVersionPreviewMode) return
|
|
115
366
|
e.preventDefault()
|
|
367
|
+
const result = pasteEditorClipboardToLevel()
|
|
368
|
+
if (result?.pastedIds.length) {
|
|
369
|
+
sfxEmitter.emit('sfx:item-place')
|
|
370
|
+
}
|
|
371
|
+
} else if (e.key.toLowerCase() === 'z' && e.shiftKey && (e.metaKey || e.ctrlKey)) {
|
|
372
|
+
if (isVersionPreviewMode) return
|
|
373
|
+
e.preventDefault()
|
|
374
|
+
if (cancelInteractionForHistoryShortcut()) return
|
|
116
375
|
runRedo()
|
|
376
|
+
} else if (e.key.toLowerCase() === 'z' && !e.shiftKey && (e.metaKey || e.ctrlKey)) {
|
|
377
|
+
if (isVersionPreviewMode) return
|
|
378
|
+
e.preventDefault()
|
|
379
|
+
if (cancelInteractionForHistoryShortcut()) return
|
|
380
|
+
runUndo()
|
|
117
381
|
} else if (e.key === 'ArrowUp' && (e.metaKey || e.ctrlKey)) {
|
|
118
382
|
e.preventDefault()
|
|
119
383
|
const { buildingId, levelId } = useViewer.getState().selection
|
|
120
384
|
if (buildingId) {
|
|
121
385
|
const building = useScene.getState().nodes[buildingId]
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
386
|
+
const levels =
|
|
387
|
+
building?.type === 'building'
|
|
388
|
+
? building.children.filter(
|
|
389
|
+
(childId) => useScene.getState().nodes[childId as AnyNodeId]?.type === 'level',
|
|
390
|
+
)
|
|
391
|
+
: []
|
|
392
|
+
if (levels.length > 0) {
|
|
393
|
+
const currentIdx = levelId ? levels.indexOf(levelId as any) : -1
|
|
394
|
+
const nextIdx = currentIdx < levels.length - 1 ? currentIdx + 1 : currentIdx
|
|
125
395
|
if (nextIdx !== -1 && nextIdx !== currentIdx) {
|
|
126
|
-
useViewer.getState().setSelection({ levelId:
|
|
396
|
+
useViewer.getState().setSelection({ levelId: levels[nextIdx] as any })
|
|
127
397
|
} else if (currentIdx === -1) {
|
|
128
|
-
useViewer.getState().setSelection({ levelId:
|
|
398
|
+
useViewer.getState().setSelection({ levelId: levels[0] as any })
|
|
129
399
|
}
|
|
130
400
|
}
|
|
131
401
|
}
|
|
@@ -134,70 +404,192 @@ export const useKeyboard = ({
|
|
|
134
404
|
const { buildingId, levelId } = useViewer.getState().selection
|
|
135
405
|
if (buildingId) {
|
|
136
406
|
const building = useScene.getState().nodes[buildingId]
|
|
137
|
-
|
|
138
|
-
|
|
407
|
+
const levels =
|
|
408
|
+
building?.type === 'building'
|
|
409
|
+
? building.children.filter(
|
|
410
|
+
(childId) => useScene.getState().nodes[childId as AnyNodeId]?.type === 'level',
|
|
411
|
+
)
|
|
412
|
+
: []
|
|
413
|
+
if (levels.length > 0) {
|
|
414
|
+
const currentIdx = levelId ? levels.indexOf(levelId as any) : -1
|
|
139
415
|
const prevIdx = currentIdx > 0 ? currentIdx - 1 : currentIdx
|
|
140
416
|
if (prevIdx !== -1 && prevIdx !== currentIdx) {
|
|
141
|
-
useViewer.getState().setSelection({ levelId:
|
|
417
|
+
useViewer.getState().setSelection({ levelId: levels[prevIdx] as any })
|
|
142
418
|
} else if (currentIdx === -1) {
|
|
143
|
-
useViewer
|
|
144
|
-
.getState()
|
|
145
|
-
.setSelection({ levelId: building.children[building.children.length - 1] as any })
|
|
419
|
+
useViewer.getState().setSelection({ levelId: levels[levels.length - 1] as any })
|
|
146
420
|
}
|
|
147
421
|
}
|
|
148
422
|
}
|
|
149
|
-
} else if (
|
|
423
|
+
} else if (
|
|
424
|
+
(e.key === 'r' || e.key === 'R') &&
|
|
425
|
+
!e.metaKey &&
|
|
426
|
+
!e.ctrlKey &&
|
|
427
|
+
!isVersionPreviewMode &&
|
|
428
|
+
!isPlacingOpening()
|
|
429
|
+
) {
|
|
430
|
+
// `!metaKey && !ctrlKey` lets Cmd/Ctrl+R reach the browser reload instead
|
|
431
|
+
// of rotating/flipping the selected node.
|
|
150
432
|
// Rotate selected node clockwise if it supports rotation (items, roofs, etc.)
|
|
151
|
-
//
|
|
433
|
+
// Doors use R to flip side (front ↔ back, rotation += π); their
|
|
434
|
+
// open/close toggle lives on E. Windows still use R to toggle
|
|
435
|
+
// their open/closed state.
|
|
436
|
+
//
|
|
437
|
+
// Skipped entirely while a door/window placement is active
|
|
438
|
+
// (`isPlacingOpening`): the placement tool owns R then (flip the draft
|
|
439
|
+
// before commit), and the user can have a node selected at the same
|
|
440
|
+
// time — without this guard both would fire (double flip + sfx).
|
|
441
|
+
//
|
|
442
|
+
// References (guide/scan) live in `selectedReferenceId`, not the viewer
|
|
443
|
+
// selection — check them first, like the Delete arm below.
|
|
444
|
+
//
|
|
445
|
+
// Multi-selection branches to the group rotate before any of the
|
|
446
|
+
// single-selection arms (reference, door/window flip, registry
|
|
447
|
+
// keyboardActions, plain rotate) — those stay single-selection-only.
|
|
448
|
+
if (rotateGroupSelection(1)) {
|
|
449
|
+
e.preventDefault()
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
const rotatableReference = getRotatableSelectedReference()
|
|
453
|
+
if (rotatableReference) {
|
|
454
|
+
e.preventDefault()
|
|
455
|
+
useScene.getState().updateNode(rotatableReference.id, {
|
|
456
|
+
rotation: [
|
|
457
|
+
rotatableReference.rotation[0],
|
|
458
|
+
steppedRotation(rotatableReference.rotation[1], 1),
|
|
459
|
+
rotatableReference.rotation[2],
|
|
460
|
+
],
|
|
461
|
+
})
|
|
462
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
463
|
+
return
|
|
464
|
+
}
|
|
152
465
|
const selectedNodeIds = useViewer.getState().selection.selectedIds as AnyNodeId[]
|
|
153
466
|
if (selectedNodeIds.length === 1) {
|
|
154
|
-
const
|
|
467
|
+
const sceneNodes = useScene.getState().nodes
|
|
468
|
+
const selectedNode = sceneNodes[selectedNodeIds[0]!]
|
|
469
|
+
const node = selectedNode ? resolveDirectManipulationNode(selectedNode, sceneNodes) : null
|
|
155
470
|
if (node?.type === 'door') {
|
|
156
471
|
e.preventDefault()
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
472
|
+
useScene.getState().updateNode(node.id, {
|
|
473
|
+
side: node.side === 'front' ? 'back' : 'front',
|
|
474
|
+
rotation: [node.rotation[0], node.rotation[1] + Math.PI, node.rotation[2]],
|
|
475
|
+
})
|
|
476
|
+
if (node.parentId) {
|
|
477
|
+
useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId)
|
|
160
478
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
node.windowType === 'casement' ||
|
|
166
|
-
node.windowType === 'awning' ||
|
|
167
|
-
node.windowType === 'hopper' ||
|
|
168
|
-
node.windowType === 'single-hung' ||
|
|
169
|
-
node.windowType === 'double-hung' ||
|
|
170
|
-
node.windowType === 'louvered')
|
|
171
|
-
) {
|
|
479
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
480
|
+
} else if (node?.type === 'window') {
|
|
481
|
+
// Windows: R flips side (front ↔ back, rotation += π). Open/
|
|
482
|
+
// close toggle for operable windows lives on E.
|
|
172
483
|
e.preventDefault()
|
|
173
|
-
|
|
484
|
+
useScene.getState().updateNode(node.id, {
|
|
485
|
+
side: node.side === 'front' ? 'back' : 'front',
|
|
486
|
+
rotation: [node.rotation[0], node.rotation[1] + Math.PI, node.rotation[2]],
|
|
487
|
+
})
|
|
488
|
+
if (node.parentId) {
|
|
489
|
+
useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId)
|
|
490
|
+
}
|
|
491
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
492
|
+
} else if (node && nodeRegistry.get(node.type)?.keyboardActions?.r?.appliesTo(node)) {
|
|
493
|
+
// Registry-driven R action. Skylight uses this for open/
|
|
494
|
+
// close toggling; future kinds with custom R behaviour
|
|
495
|
+
// declare it on their `def.keyboardActions` without
|
|
496
|
+
// touching this hook. Door / window still use the legacy
|
|
497
|
+
// direct calls above (follow-up to migrate).
|
|
498
|
+
e.preventDefault()
|
|
499
|
+
nodeRegistry.get(node.type)?.keyboardActions?.r?.run(node)
|
|
174
500
|
sfxEmitter.emit('sfx:item-rotate')
|
|
175
501
|
} else if (node && 'rotation' in node) {
|
|
176
502
|
e.preventDefault()
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
// Handle different rotation types (number for roof, array for items/windows/doors)
|
|
503
|
+
// Round to the nearest 45° then step one increment (not a blind +45°).
|
|
180
504
|
if (typeof node.rotation === 'number') {
|
|
181
|
-
useScene
|
|
505
|
+
useScene
|
|
506
|
+
.getState()
|
|
507
|
+
.updateNode(node.id, { rotation: steppedRotation(node.rotation, 1) })
|
|
182
508
|
} else if (Array.isArray(node.rotation)) {
|
|
183
509
|
useScene.getState().updateNode(node.id, {
|
|
184
|
-
rotation: [
|
|
510
|
+
rotation: [
|
|
511
|
+
node.rotation[0],
|
|
512
|
+
steppedRotation(node.rotation[1], 1),
|
|
513
|
+
node.rotation[2],
|
|
514
|
+
],
|
|
185
515
|
})
|
|
186
516
|
}
|
|
187
517
|
sfxEmitter.emit('sfx:item-rotate')
|
|
188
518
|
}
|
|
189
519
|
}
|
|
190
|
-
} else if ((e.key === 't' || e.key === 'T') && !isVersionPreviewMode) {
|
|
520
|
+
} else if ((e.key === 't' || e.key === 'T') && !isVersionPreviewMode && !isPlacingOpening()) {
|
|
191
521
|
// Rotate selected node counter-clockwise
|
|
522
|
+
// Multi-selection → group rotate, mirroring the R arm above.
|
|
523
|
+
if (rotateGroupSelection(-1)) {
|
|
524
|
+
e.preventDefault()
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
const rotatableReference = getRotatableSelectedReference()
|
|
528
|
+
if (rotatableReference) {
|
|
529
|
+
e.preventDefault()
|
|
530
|
+
useScene.getState().updateNode(rotatableReference.id, {
|
|
531
|
+
rotation: [
|
|
532
|
+
rotatableReference.rotation[0],
|
|
533
|
+
steppedRotation(rotatableReference.rotation[1], -1),
|
|
534
|
+
rotatableReference.rotation[2],
|
|
535
|
+
],
|
|
536
|
+
})
|
|
537
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
538
|
+
return
|
|
539
|
+
}
|
|
192
540
|
const selectedNodeIds = useViewer.getState().selection.selectedIds as AnyNodeId[]
|
|
193
541
|
if (selectedNodeIds.length === 1) {
|
|
194
|
-
const
|
|
542
|
+
const sceneNodes = useScene.getState().nodes
|
|
543
|
+
const selectedNode = sceneNodes[selectedNodeIds[0]!]
|
|
544
|
+
const node = selectedNode ? resolveDirectManipulationNode(selectedNode, sceneNodes) : null
|
|
195
545
|
if (node?.type === 'door') {
|
|
546
|
+
// Door's open/close moved to E; T is a no-op for doors so
|
|
547
|
+
// it doesn't free-rotate a wall-bound node by π/4.
|
|
548
|
+
e.preventDefault()
|
|
549
|
+
} else if (node?.type === 'window') {
|
|
550
|
+
// Window's open/close moved to E; T is a no-op so it doesn't
|
|
551
|
+
// free-rotate a wall-bound node by π/4.
|
|
552
|
+
e.preventDefault()
|
|
553
|
+
} else if (node && nodeRegistry.get(node.type)?.keyboardActions?.t?.appliesTo(node)) {
|
|
554
|
+
// Registry-driven T action. Same shape as the R arm above.
|
|
196
555
|
e.preventDefault()
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
556
|
+
nodeRegistry.get(node.type)?.keyboardActions?.t?.run(node)
|
|
557
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
558
|
+
} else if (node && 'rotation' in node) {
|
|
559
|
+
e.preventDefault()
|
|
560
|
+
// Round to the nearest 45° then step one increment back.
|
|
561
|
+
if (typeof node.rotation === 'number') {
|
|
562
|
+
useScene
|
|
563
|
+
.getState()
|
|
564
|
+
.updateNode(node.id, { rotation: steppedRotation(node.rotation, -1) })
|
|
565
|
+
} else if (Array.isArray(node.rotation)) {
|
|
566
|
+
useScene.getState().updateNode(node.id, {
|
|
567
|
+
rotation: [
|
|
568
|
+
node.rotation[0],
|
|
569
|
+
steppedRotation(node.rotation[1], -1),
|
|
570
|
+
node.rotation[2],
|
|
571
|
+
],
|
|
572
|
+
})
|
|
200
573
|
}
|
|
574
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
} else if ((e.key === 'e' || e.key === 'E') && !isVersionPreviewMode) {
|
|
578
|
+
// Toggle door / operable-window open/closed state. Moved off R,
|
|
579
|
+
// which now flips the opening (side + π rotation).
|
|
580
|
+
const selectedNodeIds = useViewer.getState().selection.selectedIds as AnyNodeId[]
|
|
581
|
+
if (selectedNodeIds.length === 1) {
|
|
582
|
+
const node = useScene.getState().nodes[selectedNodeIds[0]!]
|
|
583
|
+
const registryE = node && nodeRegistry.get(node.type)?.keyboardActions?.e
|
|
584
|
+
if (node && registryE?.appliesTo(node)) {
|
|
585
|
+
// Registry-driven E interaction. Same shape as the R/T arms.
|
|
586
|
+
e.preventDefault()
|
|
587
|
+
registryE.run(node)
|
|
588
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
589
|
+
} else if (node?.type === 'door' && node.openingKind !== 'opening') {
|
|
590
|
+
e.preventDefault()
|
|
591
|
+
toggleDoorOpenState(node.id)
|
|
592
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
201
593
|
} else if (
|
|
202
594
|
node?.type === 'window' &&
|
|
203
595
|
node.openingKind !== 'opening' &&
|
|
@@ -210,19 +602,7 @@ export const useKeyboard = ({
|
|
|
210
602
|
node.windowType === 'louvered')
|
|
211
603
|
) {
|
|
212
604
|
e.preventDefault()
|
|
213
|
-
|
|
214
|
-
sfxEmitter.emit('sfx:item-rotate')
|
|
215
|
-
} else if (node && 'rotation' in node) {
|
|
216
|
-
e.preventDefault()
|
|
217
|
-
const ROTATION_STEP = Math.PI / 4
|
|
218
|
-
|
|
219
|
-
if (typeof node.rotation === 'number') {
|
|
220
|
-
useScene.getState().updateNode(node.id, { rotation: node.rotation - ROTATION_STEP })
|
|
221
|
-
} else if (Array.isArray(node.rotation)) {
|
|
222
|
-
useScene.getState().updateNode(node.id, {
|
|
223
|
-
rotation: [node.rotation[0], node.rotation[1] - ROTATION_STEP, node.rotation[2]],
|
|
224
|
-
})
|
|
225
|
-
}
|
|
605
|
+
toggleWindowOpenState(node.id)
|
|
226
606
|
sfxEmitter.emit('sfx:item-rotate')
|
|
227
607
|
}
|
|
228
608
|
}
|
|
@@ -241,15 +621,6 @@ export const useKeyboard = ({
|
|
|
241
621
|
}
|
|
242
622
|
}
|
|
243
623
|
|
|
244
|
-
// Delete selected zone
|
|
245
|
-
const selectedZoneId = useViewer.getState().selection.zoneId
|
|
246
|
-
if (selectedZoneId) {
|
|
247
|
-
sfxEmitter.emit('sfx:structure-delete')
|
|
248
|
-
useScene.getState().deleteNode(selectedZoneId as AnyNodeId)
|
|
249
|
-
useViewer.getState().setSelection({ zoneId: null })
|
|
250
|
-
return
|
|
251
|
-
}
|
|
252
|
-
|
|
253
624
|
const selectedNodeIds = useViewer.getState().selection.selectedIds as AnyNodeId[]
|
|
254
625
|
|
|
255
626
|
if (selectedNodeIds.length > 0) {
|
|
@@ -265,21 +636,48 @@ export const useKeyboard = ({
|
|
|
265
636
|
// Play appropriate SFX based on what's being deleted
|
|
266
637
|
if (selectedNodeIds.length === 1) {
|
|
267
638
|
const node = useScene.getState().nodes[selectedNodeIds[0]!]
|
|
268
|
-
|
|
269
|
-
sfxEmitter.emit('sfx:item-delete')
|
|
270
|
-
} else {
|
|
271
|
-
sfxEmitter.emit('sfx:structure-delete')
|
|
272
|
-
}
|
|
639
|
+
emitDeleteSFX(node?.type)
|
|
273
640
|
} else {
|
|
274
641
|
sfxEmitter.emit('sfx:structure-delete')
|
|
275
642
|
}
|
|
276
643
|
|
|
277
644
|
useScene.getState().deleteNodes(selectedNodeIds)
|
|
645
|
+
return
|
|
278
646
|
}
|
|
647
|
+
|
|
648
|
+
// Delete selected zone when no explicit element selection is active.
|
|
649
|
+
const selectedZoneId = useViewer.getState().selection.zoneId
|
|
650
|
+
if (selectedZoneId) {
|
|
651
|
+
sfxEmitter.emit('sfx:structure-delete')
|
|
652
|
+
useScene.getState().deleteNode(selectedZoneId as AnyNodeId)
|
|
653
|
+
useViewer.getState().setSelection({ zoneId: null })
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const handleKeyUp = (e: KeyboardEvent) => {
|
|
658
|
+
if (e.key === 'Control' || e.key === 'Meta') {
|
|
659
|
+
const wasClean = ctrlTapClean
|
|
660
|
+
ctrlTapClean = false
|
|
661
|
+
if (!wasClean) return
|
|
662
|
+
// Same scope as the Shift snapping-mode cycle, and never while typing
|
|
663
|
+
// in an input.
|
|
664
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
665
|
+
return
|
|
666
|
+
}
|
|
667
|
+
if (!isSnappingCycleContext()) return
|
|
668
|
+
// Cycle the grid / measurement step (0.5 → 0.25 → 0.1 → 0.05).
|
|
669
|
+
useEditor.getState().cycleGridSnapStep()
|
|
670
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
671
|
+
return
|
|
279
672
|
}
|
|
280
673
|
}
|
|
674
|
+
|
|
281
675
|
window.addEventListener('keydown', handleKeyDown)
|
|
282
|
-
|
|
676
|
+
window.addEventListener('keyup', handleKeyUp)
|
|
677
|
+
return () => {
|
|
678
|
+
window.removeEventListener('keydown', handleKeyDown)
|
|
679
|
+
window.removeEventListener('keyup', handleKeyUp)
|
|
680
|
+
}
|
|
283
681
|
}, [disabled, isVersionPreviewMode])
|
|
284
682
|
|
|
285
683
|
return null
|