@pascal-app/editor 0.9.1 → 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 +12 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +63 -25
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -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 +36 -3
- package/src/components/editor/first-person/build-collider-world.ts +32 -5
- package/src/components/editor/first-person-controls.tsx +118 -7
- package/src/components/editor/floating-action-menu.tsx +410 -33
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- 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 +1804 -849
- package/src/components/editor/grid.tsx +166 -39
- 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 +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/use-handle-drag.ts +37 -4
- package/src/components/editor/index.tsx +248 -27
- package/src/components/editor/measurement-pill.tsx +62 -22
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +366 -173
- 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 +812 -466
- package/src/components/editor/site-edge-labels.tsx +4 -4
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +136 -43
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +3 -13
- package/src/components/editor/wall-move-side-handles.tsx +36 -21
- package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
- 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 +169 -4
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- 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-placement-preview-layer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
- 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 +80 -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 +31 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
- package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
- package/src/components/tools/roof/roof-tool.tsx +320 -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/placement-box.tsx +183 -1
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +88 -36
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +82 -11
- package/src/components/tools/tool-manager.tsx +86 -25
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +180 -103
- package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +82 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -93
- 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 +188 -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 +2 -2
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +86 -17
- package/src/components/ui/controls/material-picker.tsx +83 -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/floating-level-selector.tsx +1 -1
- 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 +227 -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/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +22 -13
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +15 -2
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- 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 +71 -40
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- 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 +40 -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-overlay.tsx +85 -10
- package/src/components/viewer-zone-system.tsx +6 -1
- 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-keyboard.ts +385 -40
- package/src/index.tsx +219 -6
- package/src/lib/active-placement-surface.ts +35 -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 +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -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 +240 -3
- package/src/lib/fresh-planar-placement.ts +68 -1
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +9 -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 +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- 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 +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/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-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 +196 -0
- package/src/lib/scene-clipboard.ts +73 -8
- package/src/lib/scene.ts +28 -5
- 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 +50 -13
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +111 -66
- 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/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 +37 -5
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +484 -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 +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 +43 -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/level-name.ts +0 -11
|
@@ -1,15 +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'
|
|
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'
|
|
4
23
|
import { toggleDoorOpenState } from '../lib/door-interaction'
|
|
24
|
+
import { guideEmitter } from '../lib/guide-events'
|
|
5
25
|
import { runRedo, runUndo } from '../lib/history'
|
|
26
|
+
import { isActive } from '../lib/interaction/scope'
|
|
6
27
|
import {
|
|
7
28
|
copySelectedNodesToEditorClipboard,
|
|
8
29
|
pasteEditorClipboardToLevel,
|
|
9
30
|
} from '../lib/scene-clipboard'
|
|
10
31
|
import { emitDeleteSFX, sfxEmitter } from '../lib/sfx-bus'
|
|
11
32
|
import { toggleWindowOpenState } from '../lib/window-interaction'
|
|
12
|
-
import useEditor from '../store/use-editor'
|
|
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
|
+
}
|
|
13
94
|
|
|
14
95
|
// Tools call this in their onCancel handler when they have an active mid-action to cancel,
|
|
15
96
|
// so that the global Escape handler knows not to also switch to select mode.
|
|
@@ -18,6 +99,64 @@ export const markToolCancelConsumed = () => {
|
|
|
18
99
|
_toolCancelConsumed = true
|
|
19
100
|
}
|
|
20
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
|
+
|
|
21
160
|
export const useKeyboard = ({
|
|
22
161
|
isVersionPreviewMode = false,
|
|
23
162
|
disabled = false,
|
|
@@ -30,39 +169,130 @@ export const useKeyboard = ({
|
|
|
30
169
|
return
|
|
31
170
|
}
|
|
32
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
|
+
|
|
33
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
|
+
|
|
34
208
|
// Don't handle shortcuts if user is typing in an input
|
|
35
209
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
|
36
210
|
return
|
|
37
211
|
}
|
|
38
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
|
+
|
|
39
279
|
if (e.key === 'Escape') {
|
|
40
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
|
+
|
|
41
289
|
_toolCancelConsumed = false
|
|
42
290
|
emitter.emit('tool:cancel')
|
|
43
291
|
|
|
44
292
|
// Only switch to select mode if no tool had an active mid-action to cancel.
|
|
45
293
|
// (e.g. mid-wall draw or mid-slab polygon should only cancel the action, not exit the tool)
|
|
46
294
|
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)
|
|
295
|
+
exitToSelectAfterUnconsumedCancel()
|
|
66
296
|
}
|
|
67
297
|
} else if (e.key === '1' && !e.metaKey && !e.ctrlKey) {
|
|
68
298
|
e.preventDefault()
|
|
@@ -81,6 +311,9 @@ export const useKeyboard = ({
|
|
|
81
311
|
e.preventDefault()
|
|
82
312
|
useEditor.getState().setPhase('furnish')
|
|
83
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')
|
|
84
317
|
useEditor.getState().setActiveSidebarPanel('items')
|
|
85
318
|
} else if (e.key === 'z' && !e.metaKey && !e.ctrlKey) {
|
|
86
319
|
if (isVersionPreviewMode) return
|
|
@@ -88,6 +321,17 @@ export const useKeyboard = ({
|
|
|
88
321
|
useEditor.getState().setPhase('structure')
|
|
89
322
|
useEditor.getState().setStructureLayer('zones')
|
|
90
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')
|
|
91
335
|
}
|
|
92
336
|
if (e.key === 'v' && !e.metaKey && !e.ctrlKey) {
|
|
93
337
|
e.preventDefault()
|
|
@@ -99,6 +343,9 @@ export const useKeyboard = ({
|
|
|
99
343
|
useEditor.getState().setPhase('structure')
|
|
100
344
|
useEditor.getState().setStructureLayer('elements')
|
|
101
345
|
useEditor.getState().setMode('build')
|
|
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')
|
|
102
349
|
} else if (e.key === 'x' && !e.metaKey && !e.ctrlKey) {
|
|
103
350
|
if (isVersionPreviewMode) return
|
|
104
351
|
e.preventDefault()
|
|
@@ -121,14 +368,16 @@ export const useKeyboard = ({
|
|
|
121
368
|
if (result?.pastedIds.length) {
|
|
122
369
|
sfxEmitter.emit('sfx:item-place')
|
|
123
370
|
}
|
|
124
|
-
} else if (e.key === 'z' && (e.metaKey || e.ctrlKey)) {
|
|
371
|
+
} else if (e.key.toLowerCase() === 'z' && e.shiftKey && (e.metaKey || e.ctrlKey)) {
|
|
125
372
|
if (isVersionPreviewMode) return
|
|
126
373
|
e.preventDefault()
|
|
127
|
-
|
|
128
|
-
|
|
374
|
+
if (cancelInteractionForHistoryShortcut()) return
|
|
375
|
+
runRedo()
|
|
376
|
+
} else if (e.key.toLowerCase() === 'z' && !e.shiftKey && (e.metaKey || e.ctrlKey)) {
|
|
129
377
|
if (isVersionPreviewMode) return
|
|
130
378
|
e.preventDefault()
|
|
131
|
-
|
|
379
|
+
if (cancelInteractionForHistoryShortcut()) return
|
|
380
|
+
runUndo()
|
|
132
381
|
} else if (e.key === 'ArrowUp' && (e.metaKey || e.ctrlKey)) {
|
|
133
382
|
e.preventDefault()
|
|
134
383
|
const { buildingId, levelId } = useViewer.getState().selection
|
|
@@ -171,14 +420,53 @@ export const useKeyboard = ({
|
|
|
171
420
|
}
|
|
172
421
|
}
|
|
173
422
|
}
|
|
174
|
-
} 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.
|
|
175
432
|
// Rotate selected node clockwise if it supports rotation (items, roofs, etc.)
|
|
176
433
|
// Doors use R to flip side (front ↔ back, rotation += π); their
|
|
177
434
|
// open/close toggle lives on E. Windows still use R to toggle
|
|
178
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
|
+
}
|
|
179
465
|
const selectedNodeIds = useViewer.getState().selection.selectedIds as AnyNodeId[]
|
|
180
466
|
if (selectedNodeIds.length === 1) {
|
|
181
|
-
const
|
|
467
|
+
const sceneNodes = useScene.getState().nodes
|
|
468
|
+
const selectedNode = sceneNodes[selectedNodeIds[0]!]
|
|
469
|
+
const node = selectedNode ? resolveDirectManipulationNode(selectedNode, sceneNodes) : null
|
|
182
470
|
if (node?.type === 'door') {
|
|
183
471
|
e.preventDefault()
|
|
184
472
|
useScene.getState().updateNode(node.id, {
|
|
@@ -212,24 +500,48 @@ export const useKeyboard = ({
|
|
|
212
500
|
sfxEmitter.emit('sfx:item-rotate')
|
|
213
501
|
} else if (node && 'rotation' in node) {
|
|
214
502
|
e.preventDefault()
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// 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°).
|
|
218
504
|
if (typeof node.rotation === 'number') {
|
|
219
|
-
useScene
|
|
505
|
+
useScene
|
|
506
|
+
.getState()
|
|
507
|
+
.updateNode(node.id, { rotation: steppedRotation(node.rotation, 1) })
|
|
220
508
|
} else if (Array.isArray(node.rotation)) {
|
|
221
509
|
useScene.getState().updateNode(node.id, {
|
|
222
|
-
rotation: [
|
|
510
|
+
rotation: [
|
|
511
|
+
node.rotation[0],
|
|
512
|
+
steppedRotation(node.rotation[1], 1),
|
|
513
|
+
node.rotation[2],
|
|
514
|
+
],
|
|
223
515
|
})
|
|
224
516
|
}
|
|
225
517
|
sfxEmitter.emit('sfx:item-rotate')
|
|
226
518
|
}
|
|
227
519
|
}
|
|
228
|
-
} else if ((e.key === 't' || e.key === 'T') && !isVersionPreviewMode) {
|
|
520
|
+
} else if ((e.key === 't' || e.key === 'T') && !isVersionPreviewMode && !isPlacingOpening()) {
|
|
229
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
|
+
}
|
|
230
540
|
const selectedNodeIds = useViewer.getState().selection.selectedIds as AnyNodeId[]
|
|
231
541
|
if (selectedNodeIds.length === 1) {
|
|
232
|
-
const
|
|
542
|
+
const sceneNodes = useScene.getState().nodes
|
|
543
|
+
const selectedNode = sceneNodes[selectedNodeIds[0]!]
|
|
544
|
+
const node = selectedNode ? resolveDirectManipulationNode(selectedNode, sceneNodes) : null
|
|
233
545
|
if (node?.type === 'door') {
|
|
234
546
|
// Door's open/close moved to E; T is a no-op for doors so
|
|
235
547
|
// it doesn't free-rotate a wall-bound node by π/4.
|
|
@@ -245,13 +557,18 @@ export const useKeyboard = ({
|
|
|
245
557
|
sfxEmitter.emit('sfx:item-rotate')
|
|
246
558
|
} else if (node && 'rotation' in node) {
|
|
247
559
|
e.preventDefault()
|
|
248
|
-
|
|
249
|
-
|
|
560
|
+
// Round to the nearest 45° then step one increment back.
|
|
250
561
|
if (typeof node.rotation === 'number') {
|
|
251
|
-
useScene
|
|
562
|
+
useScene
|
|
563
|
+
.getState()
|
|
564
|
+
.updateNode(node.id, { rotation: steppedRotation(node.rotation, -1) })
|
|
252
565
|
} else if (Array.isArray(node.rotation)) {
|
|
253
566
|
useScene.getState().updateNode(node.id, {
|
|
254
|
-
rotation: [
|
|
567
|
+
rotation: [
|
|
568
|
+
node.rotation[0],
|
|
569
|
+
steppedRotation(node.rotation[1], -1),
|
|
570
|
+
node.rotation[2],
|
|
571
|
+
],
|
|
255
572
|
})
|
|
256
573
|
}
|
|
257
574
|
sfxEmitter.emit('sfx:item-rotate')
|
|
@@ -263,7 +580,13 @@ export const useKeyboard = ({
|
|
|
263
580
|
const selectedNodeIds = useViewer.getState().selection.selectedIds as AnyNodeId[]
|
|
264
581
|
if (selectedNodeIds.length === 1) {
|
|
265
582
|
const node = useScene.getState().nodes[selectedNodeIds[0]!]
|
|
266
|
-
|
|
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') {
|
|
267
590
|
e.preventDefault()
|
|
268
591
|
toggleDoorOpenState(node.id)
|
|
269
592
|
sfxEmitter.emit('sfx:item-rotate')
|
|
@@ -331,8 +654,30 @@ export const useKeyboard = ({
|
|
|
331
654
|
}
|
|
332
655
|
}
|
|
333
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
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
334
675
|
window.addEventListener('keydown', handleKeyDown)
|
|
335
|
-
|
|
676
|
+
window.addEventListener('keyup', handleKeyUp)
|
|
677
|
+
return () => {
|
|
678
|
+
window.removeEventListener('keydown', handleKeyDown)
|
|
679
|
+
window.removeEventListener('keyup', handleKeyUp)
|
|
680
|
+
}
|
|
336
681
|
}, [disabled, isVersionPreviewMode])
|
|
337
682
|
|
|
338
683
|
return null
|