@pascal-app/editor 0.9.1 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +49 -0
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +332 -199
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
- package/src/components/editor/first-person/build-collider-world.ts +52 -5
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +410 -136
- package/src/components/editor/floating-action-menu.tsx +435 -64
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +72 -0
- package/src/components/editor/floorplan-background-selection.ts +7 -20
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +2517 -1492
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +597 -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 +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +49 -6
- package/src/components/editor/index.tsx +289 -35
- package/src/components/editor/measurement-pill.tsx +68 -23
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +409 -180
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +76 -0
- package/src/components/editor/quick-measurement-hud.tsx +34 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +814 -466
- package/src/components/editor/site-edge-labels.tsx +28 -6
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +118 -253
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
- package/src/components/editor/wall-measurement-label.tsx +10 -28
- package/src/components/editor/wall-move-side-handles.tsx +229 -39
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- 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-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +107 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +146 -57
- package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
- package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
- package/src/components/tools/roof/roof-tool.tsx +336 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- 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 +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- 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 +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +101 -42
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +164 -42
- package/src/components/tools/tool-manager.tsx +149 -30
- package/src/components/tools/wall/wall-drafting.test.ts +447 -0
- package/src/components/tools/wall/wall-drafting.ts +304 -108
- package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +98 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +44 -94
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +267 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +16 -3
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +93 -18
- package/src/components/ui/controls/material-picker.tsx +162 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- 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/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +74 -20
- 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 +273 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- 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 +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +36 -14
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +19 -4
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- 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/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- 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 +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
- 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 +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -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 +44 -3
- 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/tab-bar.tsx +42 -28
- 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/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +36 -596
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +438 -70
- package/src/index.tsx +355 -9
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +42 -0
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- 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 +275 -0
- package/src/lib/direct-manipulation.ts +118 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +236 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +978 -0
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +52 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +283 -3
- package/src/lib/fresh-planar-placement.ts +85 -1
- package/src/lib/glb-export.test.ts +556 -0
- package/src/lib/glb-export.ts +1071 -0
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +31 -0
- 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 +52 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +243 -0
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- 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 +188 -0
- package/src/lib/measurements.ts +210 -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/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- 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 +325 -0
- package/src/lib/scene-clipboard.ts +310 -21
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +313 -0
- package/src/lib/selection-routing.ts +180 -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 +82 -14
- package/src/lib/sfx-player.test.ts +266 -0
- package/src/lib/sfx-player.ts +262 -69
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -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 +165 -0
- package/src/lib/snapping-mode.ts +191 -0
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +58 -86
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +665 -126
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +51 -0
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +151 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +206 -0
- package/src/store/use-interaction-scope.ts +140 -0
- package/src/store/use-measurement-draft.test.ts +559 -0
- package/src/store/use-measurement-draft.ts +570 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-placement-preview.ts +11 -3
- 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 +53 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/floorplan/selection-tool.ts +0 -271
- package/src/lib/level-name.ts +0 -11
|
@@ -1,15 +1,100 @@
|
|
|
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'
|
|
12
|
+
import { Vector3 } from 'three'
|
|
13
|
+
import {
|
|
14
|
+
cutSelectionToEditorClipboard,
|
|
15
|
+
deleteSelection,
|
|
16
|
+
pasteSelectionAndPickUp,
|
|
17
|
+
} from '../components/editor/group-actions'
|
|
18
|
+
import {
|
|
19
|
+
classifyParticipant,
|
|
20
|
+
collectParticipants,
|
|
21
|
+
computeGroupBox,
|
|
22
|
+
expandToComponent,
|
|
23
|
+
levelFrame,
|
|
24
|
+
rotateGroupPatches,
|
|
25
|
+
} from '../components/editor/group-transform-shared'
|
|
26
|
+
import { steppedRotation } from '../components/tools/item/placement-math'
|
|
27
|
+
import { resolveDirectManipulationNode } from '../lib/direct-manipulation'
|
|
4
28
|
import { toggleDoorOpenState } from '../lib/door-interaction'
|
|
29
|
+
import { guideEmitter } from '../lib/guide-events'
|
|
5
30
|
import { runRedo, runUndo } from '../lib/history'
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from '../lib/
|
|
10
|
-
import { emitDeleteSFX, sfxEmitter } from '../lib/sfx-bus'
|
|
31
|
+
import { isActive } from '../lib/interaction/scope'
|
|
32
|
+
import { copySelectedNodesToEditorClipboard } from '../lib/scene-clipboard'
|
|
33
|
+
import { sfxEmitter } from '../lib/sfx-bus'
|
|
34
|
+
import { activeSiteNode, clampBrushRadius } from '../lib/terrain-sculpt'
|
|
11
35
|
import { toggleWindowOpenState } from '../lib/window-interaction'
|
|
12
|
-
import
|
|
36
|
+
import useDeleteConfirmation from '../store/use-delete-confirmation'
|
|
37
|
+
import useEditor, { getActiveContinuationContext, getActiveSnapContext } from '../store/use-editor'
|
|
38
|
+
import useInteractionScope, { getMovingNode } from '../store/use-interaction-scope'
|
|
39
|
+
|
|
40
|
+
// References (guide/scan) are selected via `useEditor.selectedReferenceId`, not
|
|
41
|
+
// the viewer selection, so selection-based key arms (R/T rotate) need this
|
|
42
|
+
// separate lookup. Locked guides don't rotate, matching direct manipulation.
|
|
43
|
+
function getRotatableSelectedReference() {
|
|
44
|
+
const refId = useEditor.getState().selectedReferenceId
|
|
45
|
+
if (!refId) return null
|
|
46
|
+
const node = useScene.getState().nodes[refId as AnyNodeId]
|
|
47
|
+
if (!node || (node.type !== 'guide' && node.type !== 'scan')) return null
|
|
48
|
+
if (useEditor.getState().guideUi[refId]?.locked === true) return null
|
|
49
|
+
return node
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Group rotate: R/T on a multi-selection spins the whole selection rigidly
|
|
53
|
+
// ±45° around its bbox center — the keyboard sibling of the 3D group-rotate
|
|
54
|
+
// gizmo, sharing its participant snapshot + rigid-rotation math (welded
|
|
55
|
+
// wall/fence junctions, connected-component expansion). One batched
|
|
56
|
+
// `updateNodes` call = one undo step. Returns false when the selection holds
|
|
57
|
+
// no transformable participants so the caller can fall through to the
|
|
58
|
+
// single-selection arms.
|
|
59
|
+
function rotateGroupSelection(direction: 1 | -1): boolean {
|
|
60
|
+
const { selectedIds, levelId } = useViewer.getState().selection
|
|
61
|
+
if (selectedIds.length <= 1) return false
|
|
62
|
+
const nodes = useScene.getState().nodes
|
|
63
|
+
const participantIds = selectedIds.filter(
|
|
64
|
+
(id) => classifyParticipant(nodes[id as AnyNodeId], levelId, nodes) !== null,
|
|
65
|
+
)
|
|
66
|
+
if (participantIds.length === 0) return false
|
|
67
|
+
const fullIds = expandToComponent(participantIds, nodes, levelId)
|
|
68
|
+
const { starts, links } = collectParticipants(fullIds, nodes, levelId)
|
|
69
|
+
if (starts.length === 0) return false
|
|
70
|
+
|
|
71
|
+
// Same pivot as the 3D gizmo: the selection's world bbox center, converted
|
|
72
|
+
// into the level frame before orbiting placements (a rotated building would
|
|
73
|
+
// otherwise displace the centre).
|
|
74
|
+
const box = computeGroupBox(fullIds)
|
|
75
|
+
if (!box) return false
|
|
76
|
+
const worldCenter = new Vector3(
|
|
77
|
+
(box.min.x + box.max.x) / 2,
|
|
78
|
+
box.min.y,
|
|
79
|
+
(box.min.z + box.max.z) / 2,
|
|
80
|
+
)
|
|
81
|
+
const localCenter = worldCenter.applyMatrix4(levelFrame(levelId).inverse)
|
|
82
|
+
|
|
83
|
+
// R (+45° yaw) orbits by -45° in the atan2 x→z sense: yaw = rotation - delta
|
|
84
|
+
// (see rotateGroupPatches), so keyboard direction matches the single-node
|
|
85
|
+
// steppedRotation sense.
|
|
86
|
+
const delta = -direction * (Math.PI / 4)
|
|
87
|
+
const patches = rotateGroupPatches(starts, links, { x: localCenter.x, z: localCenter.z }, delta)
|
|
88
|
+
// Space detection stays out: a rigid rotation of existing walls must not
|
|
89
|
+
// re-create the room's auto floors/ceilings at the new bearing.
|
|
90
|
+
pauseSpaceDetection()
|
|
91
|
+
useScene
|
|
92
|
+
.getState()
|
|
93
|
+
.updateNodes(patches.map(([id, data]) => ({ id, data: data as Partial<AnyNode> })))
|
|
94
|
+
resumeSpaceDetection()
|
|
95
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
13
98
|
|
|
14
99
|
// Tools call this in their onCancel handler when they have an active mid-action to cancel,
|
|
15
100
|
// so that the global Escape handler knows not to also switch to select mode.
|
|
@@ -18,6 +103,64 @@ export const markToolCancelConsumed = () => {
|
|
|
18
103
|
_toolCancelConsumed = true
|
|
19
104
|
}
|
|
20
105
|
|
|
106
|
+
// Escape's fall-through when no tool consumed the cancel: drop back to the
|
|
107
|
+
// select tool (keeping building/level context) and close panels. Tools like
|
|
108
|
+
// preset/item placement rely on this — they pass no coordinator onCancel, and
|
|
109
|
+
// it is the mode switch unmounting them that destroys the draft.
|
|
110
|
+
const exitToSelectAfterUnconsumedCancel = () => {
|
|
111
|
+
const currentPhase = useEditor.getState().phase
|
|
112
|
+
const currentStructureLayer = useEditor.getState().structureLayer
|
|
113
|
+
|
|
114
|
+
useInteractionScope.getState().endIf((sc) => sc.kind === 'reshaping' && sc.reshape === 'hole')
|
|
115
|
+
|
|
116
|
+
// From zone mode, return to structure select
|
|
117
|
+
if (currentPhase === 'structure' && currentStructureLayer === 'zones') {
|
|
118
|
+
useEditor.getState().setStructureLayer('elements')
|
|
119
|
+
useEditor.getState().setMode('select')
|
|
120
|
+
} else {
|
|
121
|
+
// Return to the default select tool while keeping the active building/level context.
|
|
122
|
+
useEditor.getState().setMode('select')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
useEditor.getState().setFloorplanSelectionTool('click')
|
|
126
|
+
|
|
127
|
+
// Clear selections to close UI panels, but KEEP the active building and level context.
|
|
128
|
+
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
129
|
+
useEditor.getState().setSelectedReferenceId(null)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ⌘Z pressed mid-interaction (moving a node, drawing a wall, mid-placement…)
|
|
133
|
+
// reads as "abort this action", not history undo — behave exactly like Escape
|
|
134
|
+
// and report whether anything was in flight so the undo/redo arms know to
|
|
135
|
+
// skip the history jump. Pointer drags that only listen for their own
|
|
136
|
+
// capture-phase keydown never reach here — they stopPropagation first (see
|
|
137
|
+
// isHistoryShortcut call sites).
|
|
138
|
+
const cancelInteractionForHistoryShortcut = () => {
|
|
139
|
+
if (useEditor.getState().referenceScaleActiveGuideId) {
|
|
140
|
+
guideEmitter.emit('guide:cancel-reference-scale')
|
|
141
|
+
return true
|
|
142
|
+
}
|
|
143
|
+
_toolCancelConsumed = false
|
|
144
|
+
emitter.emit('tool:cancel')
|
|
145
|
+
if (_toolCancelConsumed) return true
|
|
146
|
+
if (
|
|
147
|
+
isActive(useInteractionScope.getState().scope) ||
|
|
148
|
+
useViewer.getState().inputDragging ||
|
|
149
|
+
// Paused history means a gesture session is live (draft placement, adopted
|
|
150
|
+
// move, …) even when no scope/drag flag is set — the preset/item draft
|
|
151
|
+
// cycle keeps temporal paused for the whole session, and a history jump
|
|
152
|
+
// against a paused store would land on a stale baseline anyway.
|
|
153
|
+
!useScene.temporal.getState().isTracking
|
|
154
|
+
) {
|
|
155
|
+
// A gesture is live but nothing consumed the cancel: finish it the way
|
|
156
|
+
// Escape does — the mode switch is what actually cancels tools that hook
|
|
157
|
+
// their teardown to unmount (preset/item placement).
|
|
158
|
+
exitToSelectAfterUnconsumedCancel()
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
|
|
21
164
|
export const useKeyboard = ({
|
|
22
165
|
isVersionPreviewMode = false,
|
|
23
166
|
disabled = false,
|
|
@@ -30,39 +173,162 @@ export const useKeyboard = ({
|
|
|
30
173
|
return
|
|
31
174
|
}
|
|
32
175
|
|
|
176
|
+
// True while a door/window is being placed: either a fresh clone is moving
|
|
177
|
+
// (preset / duplicate path) or a door/window build tool is armed. The
|
|
178
|
+
// placement tool owns R/T then (flip the draft before commit), so the
|
|
179
|
+
// global selection-based R/T handler must stand down to avoid double-firing.
|
|
180
|
+
const isPlacingOpening = () => {
|
|
181
|
+
const ed = useEditor.getState()
|
|
182
|
+
const moving = getMovingNode()
|
|
183
|
+
if (moving?.type === 'door' || moving?.type === 'window') return true
|
|
184
|
+
return ed.mode === 'build' && (ed.tool === 'door' || ed.tool === 'window')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Shift cycles the snapping mode (and a clean-tap Ctrl the grid step)
|
|
188
|
+
// whenever there's an active snapping context — i.e. exactly when the HUD
|
|
189
|
+
// shows a snapping chip. That single source covers wall/fence/item drafting,
|
|
190
|
+
// every node move (including wall-hosted items + door/window openings, which
|
|
191
|
+
// now declare `snapProfile`), and endpoint/polygon reshaping, so the keys
|
|
192
|
+
// never silently stop working. Force-place lives on Alt where a tool supports it.
|
|
193
|
+
const isSnappingCycleContext = () => getActiveSnapContext() != null
|
|
194
|
+
// A "clean tap" of Ctrl/Meta (pressed and released with NO other key in
|
|
195
|
+
// between) cycles the grid step — same context as the Shift snapping-mode
|
|
196
|
+
// cycle. `ctrlTapClean` starts true the moment Ctrl/Meta goes down alone
|
|
197
|
+
// and is cleared the instant any other key fires, so chords like Ctrl+Z /
|
|
198
|
+
// Ctrl+C never cycle.
|
|
199
|
+
let ctrlTapClean = false
|
|
200
|
+
|
|
33
201
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
202
|
+
if (e.key === 'Control' || e.key === 'Meta') {
|
|
203
|
+
// Only a fresh, modifier-free press starts a clean-tap candidate;
|
|
204
|
+
// ignore key-repeat and presses already part of a combo.
|
|
205
|
+
ctrlTapClean = !e.repeat && !e.shiftKey && !e.altKey
|
|
206
|
+
} else {
|
|
207
|
+
// Any non-modifier key (or a modifier combined with Ctrl/Meta) breaks
|
|
208
|
+
// the clean tap.
|
|
209
|
+
ctrlTapClean = false
|
|
210
|
+
}
|
|
211
|
+
|
|
34
212
|
// Don't handle shortcuts if user is typing in an input
|
|
35
213
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
36
214
|
return
|
|
37
215
|
}
|
|
38
216
|
|
|
217
|
+
if (useDeleteConfirmation.getState().request) {
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (e.key === 'Shift' && !e.repeat && useEditor.getState().mode === 'material-paint') {
|
|
222
|
+
// In paint mode Shift cycles the application scope (this surface →
|
|
223
|
+
// whole item / all matching / room) — the paint-mode analogue of the
|
|
224
|
+
// snapping-mode cycle below. The scope chip mirrors this key.
|
|
225
|
+
e.preventDefault()
|
|
226
|
+
useEditor.getState().cyclePaintScope()
|
|
227
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Brush size, on the keys every sculpting tool in the industry uses. Gated
|
|
232
|
+
// on sculpt mode so `[`/`]` stay free everywhere else. Key-repeat is
|
|
233
|
+
// allowed (unlike the cycles above) because holding to resize is the
|
|
234
|
+
// expected feel, and the step is multiplicative so one press is a
|
|
235
|
+
// proportional change at both the floor and 20 m rather than 40× coarser at
|
|
236
|
+
// the bottom of the range. The range comes from `brushRadiusRange` so this
|
|
237
|
+
// and the panel's slider cannot disagree about it — and so the low end
|
|
238
|
+
// tracks the field's sample spacing, below which a dab paints nothing.
|
|
239
|
+
if (
|
|
240
|
+
(e.key === '[' || e.key === ']') &&
|
|
241
|
+
!e.metaKey &&
|
|
242
|
+
!e.ctrlKey &&
|
|
243
|
+
useEditor.getState().mode === 'terrain-sculpt'
|
|
244
|
+
) {
|
|
245
|
+
e.preventDefault()
|
|
246
|
+
const { terrainBrush, setTerrainBrush } = useEditor.getState()
|
|
247
|
+
const factor = e.key === ']' ? 1.25 : 1 / 1.25
|
|
248
|
+
const radius = clampBrushRadius(
|
|
249
|
+
activeSiteNode(),
|
|
250
|
+
Math.round(terrainBrush.radius * factor * 10) / 10,
|
|
251
|
+
)
|
|
252
|
+
if (radius !== terrainBrush.radius) {
|
|
253
|
+
setTerrainBrush({ radius })
|
|
254
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
255
|
+
}
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (e.key === 'Shift' && !e.repeat && isSnappingCycleContext()) {
|
|
260
|
+
// Cycle the global snapping mode (grid → lines → angles → off).
|
|
261
|
+
// `'off'` is the snap bypass now, so Shift no longer holds-to-bypass.
|
|
262
|
+
e.preventDefault()
|
|
263
|
+
useEditor.getState().cycleSnappingMode()
|
|
264
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (
|
|
269
|
+
(e.key === 't' || e.key === 'T') &&
|
|
270
|
+
!e.repeat &&
|
|
271
|
+
!e.metaKey &&
|
|
272
|
+
!e.ctrlKey &&
|
|
273
|
+
!e.shiftKey &&
|
|
274
|
+
!e.altKey
|
|
275
|
+
) {
|
|
276
|
+
const context = getActiveContinuationContext()
|
|
277
|
+
if (context === 'fence') {
|
|
278
|
+
e.preventDefault()
|
|
279
|
+
const current = useEditor.getState().getContinuation('fence')
|
|
280
|
+
useEditor
|
|
281
|
+
.getState()
|
|
282
|
+
.setContinuation('fence', current === 'curved' ? 'continuous' : 'curved')
|
|
283
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (
|
|
289
|
+
(e.key === 'c' || e.key === 'C') &&
|
|
290
|
+
!e.repeat &&
|
|
291
|
+
!e.metaKey &&
|
|
292
|
+
!e.ctrlKey &&
|
|
293
|
+
!e.shiftKey &&
|
|
294
|
+
!e.altKey
|
|
295
|
+
) {
|
|
296
|
+
const context = getActiveContinuationContext()
|
|
297
|
+
if (context) {
|
|
298
|
+
e.preventDefault()
|
|
299
|
+
if (context === 'fence') {
|
|
300
|
+
const current = useEditor.getState().getContinuation('fence')
|
|
301
|
+
if (current !== 'curved') {
|
|
302
|
+
useEditor
|
|
303
|
+
.getState()
|
|
304
|
+
.setContinuation('fence', current === 'single' ? 'continuous' : 'single')
|
|
305
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
306
|
+
}
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
useEditor.getState().cycleContinuation(context)
|
|
310
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
39
315
|
if (e.key === 'Escape') {
|
|
40
316
|
e.preventDefault()
|
|
317
|
+
|
|
318
|
+
// An in-flight reference-scale measurement swallows Escape whole:
|
|
319
|
+
// cancel the flow but keep the reference selected and its panel open.
|
|
320
|
+
if (useEditor.getState().referenceScaleActiveGuideId) {
|
|
321
|
+
guideEmitter.emit('guide:cancel-reference-scale')
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
41
325
|
_toolCancelConsumed = false
|
|
42
326
|
emitter.emit('tool:cancel')
|
|
43
327
|
|
|
44
328
|
// Only switch to select mode if no tool had an active mid-action to cancel.
|
|
45
329
|
// (e.g. mid-wall draw or mid-slab polygon should only cancel the action, not exit the tool)
|
|
46
330
|
if (!_toolCancelConsumed) {
|
|
47
|
-
|
|
48
|
-
const currentStructureLayer = useEditor.getState().structureLayer
|
|
49
|
-
|
|
50
|
-
useEditor.getState().setEditingHole(null)
|
|
51
|
-
|
|
52
|
-
// From zone mode, return to structure select
|
|
53
|
-
if (currentPhase === 'structure' && currentStructureLayer === 'zones') {
|
|
54
|
-
useEditor.getState().setStructureLayer('elements')
|
|
55
|
-
useEditor.getState().setMode('select')
|
|
56
|
-
} else {
|
|
57
|
-
// Return to the default select tool while keeping the active building/level context.
|
|
58
|
-
useEditor.getState().setMode('select')
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
useEditor.getState().setFloorplanSelectionTool('click')
|
|
62
|
-
|
|
63
|
-
// Clear selections to close UI panels, but KEEP the active building and level context.
|
|
64
|
-
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
65
|
-
useEditor.getState().setSelectedReferenceId(null)
|
|
331
|
+
exitToSelectAfterUnconsumedCancel()
|
|
66
332
|
}
|
|
67
333
|
} else if (e.key === '1' && !e.metaKey && !e.ctrlKey) {
|
|
68
334
|
e.preventDefault()
|
|
@@ -81,6 +347,9 @@ export const useKeyboard = ({
|
|
|
81
347
|
e.preventDefault()
|
|
82
348
|
useEditor.getState().setPhase('furnish')
|
|
83
349
|
useEditor.getState().setMode('build')
|
|
350
|
+
// Set the item tool explicitly so the active tool never inherits a
|
|
351
|
+
// stale tool from a prior build session.
|
|
352
|
+
useEditor.getState().setTool('item')
|
|
84
353
|
useEditor.getState().setActiveSidebarPanel('items')
|
|
85
354
|
} else if (e.key === 'z' && !e.metaKey && !e.ctrlKey) {
|
|
86
355
|
if (isVersionPreviewMode) return
|
|
@@ -88,6 +357,17 @@ export const useKeyboard = ({
|
|
|
88
357
|
useEditor.getState().setPhase('structure')
|
|
89
358
|
useEditor.getState().setStructureLayer('zones')
|
|
90
359
|
useEditor.getState().setMode('build')
|
|
360
|
+
// Set the zone tool explicitly so it never inherits a stale tool.
|
|
361
|
+
useEditor.getState().setTool('zone')
|
|
362
|
+
} else if (e.key === 'm' && !e.metaKey && !e.ctrlKey) {
|
|
363
|
+
if (isVersionPreviewMode) return
|
|
364
|
+
e.preventDefault()
|
|
365
|
+
const editor = useEditor.getState()
|
|
366
|
+
editor.setPhase('structure')
|
|
367
|
+
editor.setStructureLayer('elements')
|
|
368
|
+
editor.setToolDefaults('measurement', { kind: editor.lastMeasurementKind })
|
|
369
|
+
editor.setMode('build')
|
|
370
|
+
editor.setTool('measurement')
|
|
91
371
|
}
|
|
92
372
|
if (e.key === 'v' && !e.metaKey && !e.ctrlKey) {
|
|
93
373
|
e.preventDefault()
|
|
@@ -99,6 +379,9 @@ export const useKeyboard = ({
|
|
|
99
379
|
useEditor.getState().setPhase('structure')
|
|
100
380
|
useEditor.getState().setStructureLayer('elements')
|
|
101
381
|
useEditor.getState().setMode('build')
|
|
382
|
+
// Set the wall tool explicitly so B never inherits a stale tool
|
|
383
|
+
// (e.g. fence) left over from a prior build session.
|
|
384
|
+
useEditor.getState().setTool('wall')
|
|
102
385
|
} else if (e.key === 'x' && !e.metaKey && !e.ctrlKey) {
|
|
103
386
|
if (isVersionPreviewMode) return
|
|
104
387
|
e.preventDefault()
|
|
@@ -110,25 +393,34 @@ export const useKeyboard = ({
|
|
|
110
393
|
useEditor.getState().setPhase('structure')
|
|
111
394
|
useEditor.getState().setStructureLayer('elements')
|
|
112
395
|
useEditor.getState().setMode('material-paint')
|
|
396
|
+
} else if (e.key === 'g' && !e.metaKey && !e.ctrlKey) {
|
|
397
|
+
if (isVersionPreviewMode) return
|
|
398
|
+
e.preventDefault()
|
|
399
|
+
// G for ground. No `setPhase` — `setMode` moves to the site phase itself,
|
|
400
|
+
// and doing it here would set the phase twice with a mode reset between.
|
|
401
|
+
useEditor.getState().setMode('terrain-sculpt')
|
|
113
402
|
} else if (e.key === 'c' && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
|
|
114
403
|
if (isVersionPreviewMode) return
|
|
115
404
|
e.preventDefault()
|
|
116
405
|
copySelectedNodesToEditorClipboard()
|
|
117
|
-
} else if (e.key === '
|
|
406
|
+
} else if (e.key === 'x' && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
|
|
118
407
|
if (isVersionPreviewMode) return
|
|
119
408
|
e.preventDefault()
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
sfxEmitter.emit('sfx:item-place')
|
|
123
|
-
}
|
|
124
|
-
} else if (e.key === 'z' && (e.metaKey || e.ctrlKey)) {
|
|
409
|
+
cutSelectionToEditorClipboard()
|
|
410
|
+
} else if (e.key === 'v' && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
|
|
125
411
|
if (isVersionPreviewMode) return
|
|
126
412
|
e.preventDefault()
|
|
127
|
-
|
|
128
|
-
} else if (e.key === '
|
|
413
|
+
void pasteSelectionAndPickUp()
|
|
414
|
+
} else if (e.key.toLowerCase() === 'z' && e.shiftKey && (e.metaKey || e.ctrlKey)) {
|
|
129
415
|
if (isVersionPreviewMode) return
|
|
130
416
|
e.preventDefault()
|
|
417
|
+
if (cancelInteractionForHistoryShortcut()) return
|
|
131
418
|
runRedo()
|
|
419
|
+
} else if (e.key.toLowerCase() === 'z' && !e.shiftKey && (e.metaKey || e.ctrlKey)) {
|
|
420
|
+
if (isVersionPreviewMode) return
|
|
421
|
+
e.preventDefault()
|
|
422
|
+
if (cancelInteractionForHistoryShortcut()) return
|
|
423
|
+
runUndo()
|
|
132
424
|
} else if (e.key === 'ArrowUp' && (e.metaKey || e.ctrlKey)) {
|
|
133
425
|
e.preventDefault()
|
|
134
426
|
const { buildingId, levelId } = useViewer.getState().selection
|
|
@@ -171,14 +463,53 @@ export const useKeyboard = ({
|
|
|
171
463
|
}
|
|
172
464
|
}
|
|
173
465
|
}
|
|
174
|
-
} else if (
|
|
466
|
+
} else if (
|
|
467
|
+
(e.key === 'r' || e.key === 'R') &&
|
|
468
|
+
!e.metaKey &&
|
|
469
|
+
!e.ctrlKey &&
|
|
470
|
+
!isVersionPreviewMode &&
|
|
471
|
+
!isPlacingOpening()
|
|
472
|
+
) {
|
|
473
|
+
// `!metaKey && !ctrlKey` lets Cmd/Ctrl+R reach the browser reload instead
|
|
474
|
+
// of rotating/flipping the selected node.
|
|
175
475
|
// Rotate selected node clockwise if it supports rotation (items, roofs, etc.)
|
|
176
476
|
// Doors use R to flip side (front ↔ back, rotation += π); their
|
|
177
477
|
// open/close toggle lives on E. Windows still use R to toggle
|
|
178
478
|
// their open/closed state.
|
|
479
|
+
//
|
|
480
|
+
// Skipped entirely while a door/window placement is active
|
|
481
|
+
// (`isPlacingOpening`): the placement tool owns R then (flip the draft
|
|
482
|
+
// before commit), and the user can have a node selected at the same
|
|
483
|
+
// time — without this guard both would fire (double flip + sfx).
|
|
484
|
+
//
|
|
485
|
+
// References (guide/scan) live in `selectedReferenceId`, not the viewer
|
|
486
|
+
// selection — check them first, like the Delete arm below.
|
|
487
|
+
//
|
|
488
|
+
// Multi-selection branches to the group rotate before any of the
|
|
489
|
+
// single-selection arms (reference, door/window flip, registry
|
|
490
|
+
// keyboardActions, plain rotate) — those stay single-selection-only.
|
|
491
|
+
if (rotateGroupSelection(1)) {
|
|
492
|
+
e.preventDefault()
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
const rotatableReference = getRotatableSelectedReference()
|
|
496
|
+
if (rotatableReference) {
|
|
497
|
+
e.preventDefault()
|
|
498
|
+
useScene.getState().updateNode(rotatableReference.id, {
|
|
499
|
+
rotation: [
|
|
500
|
+
rotatableReference.rotation[0],
|
|
501
|
+
steppedRotation(rotatableReference.rotation[1], 1),
|
|
502
|
+
rotatableReference.rotation[2],
|
|
503
|
+
],
|
|
504
|
+
})
|
|
505
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
506
|
+
return
|
|
507
|
+
}
|
|
179
508
|
const selectedNodeIds = useViewer.getState().selection.selectedIds as AnyNodeId[]
|
|
180
509
|
if (selectedNodeIds.length === 1) {
|
|
181
|
-
const
|
|
510
|
+
const sceneNodes = useScene.getState().nodes
|
|
511
|
+
const selectedNode = sceneNodes[selectedNodeIds[0]!]
|
|
512
|
+
const node = selectedNode ? resolveDirectManipulationNode(selectedNode, sceneNodes) : null
|
|
182
513
|
if (node?.type === 'door') {
|
|
183
514
|
e.preventDefault()
|
|
184
515
|
useScene.getState().updateNode(node.id, {
|
|
@@ -212,24 +543,48 @@ export const useKeyboard = ({
|
|
|
212
543
|
sfxEmitter.emit('sfx:item-rotate')
|
|
213
544
|
} else if (node && 'rotation' in node) {
|
|
214
545
|
e.preventDefault()
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// Handle different rotation types (number for roof, array for items/windows/doors)
|
|
546
|
+
// Round to the nearest 45° then step one increment (not a blind +45°).
|
|
218
547
|
if (typeof node.rotation === 'number') {
|
|
219
|
-
useScene
|
|
548
|
+
useScene
|
|
549
|
+
.getState()
|
|
550
|
+
.updateNode(node.id, { rotation: steppedRotation(node.rotation, 1) })
|
|
220
551
|
} else if (Array.isArray(node.rotation)) {
|
|
221
552
|
useScene.getState().updateNode(node.id, {
|
|
222
|
-
rotation: [
|
|
553
|
+
rotation: [
|
|
554
|
+
node.rotation[0],
|
|
555
|
+
steppedRotation(node.rotation[1], 1),
|
|
556
|
+
node.rotation[2],
|
|
557
|
+
],
|
|
223
558
|
})
|
|
224
559
|
}
|
|
225
560
|
sfxEmitter.emit('sfx:item-rotate')
|
|
226
561
|
}
|
|
227
562
|
}
|
|
228
|
-
} else if ((e.key === 't' || e.key === 'T') && !isVersionPreviewMode) {
|
|
563
|
+
} else if ((e.key === 't' || e.key === 'T') && !isVersionPreviewMode && !isPlacingOpening()) {
|
|
229
564
|
// Rotate selected node counter-clockwise
|
|
565
|
+
// Multi-selection → group rotate, mirroring the R arm above.
|
|
566
|
+
if (rotateGroupSelection(-1)) {
|
|
567
|
+
e.preventDefault()
|
|
568
|
+
return
|
|
569
|
+
}
|
|
570
|
+
const rotatableReference = getRotatableSelectedReference()
|
|
571
|
+
if (rotatableReference) {
|
|
572
|
+
e.preventDefault()
|
|
573
|
+
useScene.getState().updateNode(rotatableReference.id, {
|
|
574
|
+
rotation: [
|
|
575
|
+
rotatableReference.rotation[0],
|
|
576
|
+
steppedRotation(rotatableReference.rotation[1], -1),
|
|
577
|
+
rotatableReference.rotation[2],
|
|
578
|
+
],
|
|
579
|
+
})
|
|
580
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
581
|
+
return
|
|
582
|
+
}
|
|
230
583
|
const selectedNodeIds = useViewer.getState().selection.selectedIds as AnyNodeId[]
|
|
231
584
|
if (selectedNodeIds.length === 1) {
|
|
232
|
-
const
|
|
585
|
+
const sceneNodes = useScene.getState().nodes
|
|
586
|
+
const selectedNode = sceneNodes[selectedNodeIds[0]!]
|
|
587
|
+
const node = selectedNode ? resolveDirectManipulationNode(selectedNode, sceneNodes) : null
|
|
233
588
|
if (node?.type === 'door') {
|
|
234
589
|
// Door's open/close moved to E; T is a no-op for doors so
|
|
235
590
|
// it doesn't free-rotate a wall-bound node by π/4.
|
|
@@ -245,13 +600,18 @@ export const useKeyboard = ({
|
|
|
245
600
|
sfxEmitter.emit('sfx:item-rotate')
|
|
246
601
|
} else if (node && 'rotation' in node) {
|
|
247
602
|
e.preventDefault()
|
|
248
|
-
|
|
249
|
-
|
|
603
|
+
// Round to the nearest 45° then step one increment back.
|
|
250
604
|
if (typeof node.rotation === 'number') {
|
|
251
|
-
useScene
|
|
605
|
+
useScene
|
|
606
|
+
.getState()
|
|
607
|
+
.updateNode(node.id, { rotation: steppedRotation(node.rotation, -1) })
|
|
252
608
|
} else if (Array.isArray(node.rotation)) {
|
|
253
609
|
useScene.getState().updateNode(node.id, {
|
|
254
|
-
rotation: [
|
|
610
|
+
rotation: [
|
|
611
|
+
node.rotation[0],
|
|
612
|
+
steppedRotation(node.rotation[1], -1),
|
|
613
|
+
node.rotation[2],
|
|
614
|
+
],
|
|
255
615
|
})
|
|
256
616
|
}
|
|
257
617
|
sfxEmitter.emit('sfx:item-rotate')
|
|
@@ -263,7 +623,13 @@ export const useKeyboard = ({
|
|
|
263
623
|
const selectedNodeIds = useViewer.getState().selection.selectedIds as AnyNodeId[]
|
|
264
624
|
if (selectedNodeIds.length === 1) {
|
|
265
625
|
const node = useScene.getState().nodes[selectedNodeIds[0]!]
|
|
266
|
-
|
|
626
|
+
const registryE = node && nodeRegistry.get(node.type)?.keyboardActions?.e
|
|
627
|
+
if (node && registryE?.appliesTo(node)) {
|
|
628
|
+
// Registry-driven E interaction. Same shape as the R/T arms.
|
|
629
|
+
e.preventDefault()
|
|
630
|
+
registryE.run(node)
|
|
631
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
632
|
+
} else if (node?.type === 'door' && node.openingKind !== 'opening') {
|
|
267
633
|
e.preventDefault()
|
|
268
634
|
toggleDoorOpenState(node.id)
|
|
269
635
|
sfxEmitter.emit('sfx:item-rotate')
|
|
@@ -298,27 +664,7 @@ export const useKeyboard = ({
|
|
|
298
664
|
}
|
|
299
665
|
}
|
|
300
666
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (selectedNodeIds.length > 0) {
|
|
304
|
-
// Guard against accidental bulk deletion (e.g. box-select all + Delete)
|
|
305
|
-
const BULK_DELETE_THRESHOLD = 10
|
|
306
|
-
if (selectedNodeIds.length >= BULK_DELETE_THRESHOLD) {
|
|
307
|
-
const confirmed = window.confirm(
|
|
308
|
-
`Delete ${selectedNodeIds.length} selected elements? This cannot be undone if the undo history is exhausted.`,
|
|
309
|
-
)
|
|
310
|
-
if (!confirmed) return
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Play appropriate SFX based on what's being deleted
|
|
314
|
-
if (selectedNodeIds.length === 1) {
|
|
315
|
-
const node = useScene.getState().nodes[selectedNodeIds[0]!]
|
|
316
|
-
emitDeleteSFX(node?.type)
|
|
317
|
-
} else {
|
|
318
|
-
sfxEmitter.emit('sfx:structure-delete')
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
useScene.getState().deleteNodes(selectedNodeIds)
|
|
667
|
+
if (deleteSelection()) {
|
|
322
668
|
return
|
|
323
669
|
}
|
|
324
670
|
|
|
@@ -331,8 +677,30 @@ export const useKeyboard = ({
|
|
|
331
677
|
}
|
|
332
678
|
}
|
|
333
679
|
}
|
|
680
|
+
const handleKeyUp = (e: KeyboardEvent) => {
|
|
681
|
+
if (e.key === 'Control' || e.key === 'Meta') {
|
|
682
|
+
const wasClean = ctrlTapClean
|
|
683
|
+
ctrlTapClean = false
|
|
684
|
+
if (!wasClean) return
|
|
685
|
+
// Same scope as the Shift snapping-mode cycle, and never while typing
|
|
686
|
+
// in an input.
|
|
687
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
688
|
+
return
|
|
689
|
+
}
|
|
690
|
+
if (!isSnappingCycleContext()) return
|
|
691
|
+
// Cycle the grid / measurement step (0.5 → 0.25 → 0.1 → 0.05).
|
|
692
|
+
useEditor.getState().cycleGridSnapStep()
|
|
693
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
694
|
+
return
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
334
698
|
window.addEventListener('keydown', handleKeyDown)
|
|
335
|
-
|
|
699
|
+
window.addEventListener('keyup', handleKeyUp)
|
|
700
|
+
return () => {
|
|
701
|
+
window.removeEventListener('keydown', handleKeyDown)
|
|
702
|
+
window.removeEventListener('keyup', handleKeyUp)
|
|
703
|
+
}
|
|
336
704
|
}, [disabled, isVersionPreviewMode])
|
|
337
705
|
|
|
338
706
|
return null
|