@pascal-app/editor 0.8.0 → 0.9.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 +8 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +754 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +58 -51
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +201 -83
- package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
- package/src/components/editor/first-person-controls.tsx +825 -20
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5829 -12005
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +178 -149
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +433 -156
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +136 -62
- package/src/components/editor/thumbnail-generator.tsx +9 -3
- package/src/components/editor/use-floorplan-background-placement.ts +119 -32
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +41 -105
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +31 -5
- package/src/components/tools/item/placement-strategies.ts +190 -15
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -538
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +56 -0
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +16 -6
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/control-modes.tsx +5 -82
- package/src/components/ui/action-menu/index.tsx +1 -113
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +243 -6
- package/src/components/ui/command-palette/editor-commands.tsx +18 -13
- package/src/components/ui/command-palette/index.tsx +2 -1
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +11 -32
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/helper-manager.tsx +16 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/panel-manager.tsx +35 -47
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +10 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +115 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +237 -5
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.ts +54 -42
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +211 -19
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +23 -1
- package/src/lib/sfx-player.ts +70 -22
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +320 -37
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -280
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNodeId,
|
|
3
|
+
type BuildingNode,
|
|
4
|
+
collectAlignmentAnchors,
|
|
5
|
+
ElevatorNode,
|
|
6
|
+
emitter,
|
|
7
|
+
type GridEvent,
|
|
8
|
+
type LevelNode,
|
|
9
|
+
resolveAlignment,
|
|
10
|
+
useScene,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import { useAlignmentGuides } from '@pascal-app/editor'
|
|
13
|
+
import { useEffect, useMemo, useRef } from 'react'
|
|
14
|
+
import * as THREE from 'three'
|
|
15
|
+
import { resolveCurrentBuildingId, resolveElevatorSupportY } from '../../../lib/elevator-support'
|
|
16
|
+
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
17
|
+
import usePlacementPreview from '../../../store/use-placement-preview'
|
|
18
|
+
import { CursorSphere } from '../shared/cursor-sphere'
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_ELEVATOR_CAB_HEIGHT,
|
|
21
|
+
DEFAULT_ELEVATOR_DEPTH,
|
|
22
|
+
DEFAULT_ELEVATOR_DOOR_DURATION_MS,
|
|
23
|
+
DEFAULT_ELEVATOR_DOOR_HEIGHT,
|
|
24
|
+
DEFAULT_ELEVATOR_DOOR_WIDTH,
|
|
25
|
+
DEFAULT_ELEVATOR_DWELL_MS,
|
|
26
|
+
DEFAULT_ELEVATOR_SPEED,
|
|
27
|
+
DEFAULT_ELEVATOR_WIDTH,
|
|
28
|
+
} from './elevator-defaults'
|
|
29
|
+
|
|
30
|
+
const GRID_OFFSET = 0.02
|
|
31
|
+
/** Figma-style alignment-snap threshold (meters), matching the move tools. */
|
|
32
|
+
const ALIGNMENT_THRESHOLD_M = 0.08
|
|
33
|
+
|
|
34
|
+
type ElevatorToolProps = {
|
|
35
|
+
buildingId: BuildingNode['id'] | null
|
|
36
|
+
levelId: LevelNode['id'] | null
|
|
37
|
+
onPlaced?: (elevatorId: AnyNodeId, buildingId: BuildingNode['id']) => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveDefaultServiceRange(
|
|
41
|
+
buildingId: BuildingNode['id'],
|
|
42
|
+
selectedLevelId: LevelNode['id'] | null,
|
|
43
|
+
): {
|
|
44
|
+
defaultLevelId: LevelNode['id'] | null
|
|
45
|
+
fromLevelId: LevelNode['id'] | null
|
|
46
|
+
toLevelId: LevelNode['id'] | null
|
|
47
|
+
} {
|
|
48
|
+
const nodes = useScene.getState().nodes
|
|
49
|
+
const building = nodes[buildingId as AnyNodeId]
|
|
50
|
+
if (building?.type !== 'building') {
|
|
51
|
+
return { defaultLevelId: null, fromLevelId: null, toLevelId: null }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const levels = building.children
|
|
55
|
+
.map((childId) => nodes[childId as AnyNodeId])
|
|
56
|
+
.filter((node): node is LevelNode => node?.type === 'level')
|
|
57
|
+
.sort((left, right) => left.level - right.level)
|
|
58
|
+
const selectedLevelIndex = levels.findIndex((level) => level.id === selectedLevelId)
|
|
59
|
+
const fromIndex = selectedLevelIndex >= 0 ? selectedLevelIndex : 0
|
|
60
|
+
const fromLevel = levels[fromIndex]
|
|
61
|
+
const toLevel = levels[Math.min(fromIndex + 1, levels.length - 1)] ?? fromLevel
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
defaultLevelId: fromLevel?.id ?? null,
|
|
65
|
+
fromLevelId: fromLevel?.id ?? null,
|
|
66
|
+
toLevelId: toLevel?.id ?? fromLevel?.id ?? null,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createElevatorPreviewGeometry(): THREE.BufferGeometry {
|
|
71
|
+
return new THREE.BoxGeometry(
|
|
72
|
+
DEFAULT_ELEVATOR_WIDTH,
|
|
73
|
+
DEFAULT_ELEVATOR_CAB_HEIGHT,
|
|
74
|
+
DEFAULT_ELEVATOR_DEPTH,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function createElevatorPreviewEdgeGeometry(): THREE.BufferGeometry {
|
|
79
|
+
return new THREE.EdgesGeometry(createElevatorPreviewGeometry())
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function commitElevatorPlacement(
|
|
83
|
+
buildingId: BuildingNode['id'],
|
|
84
|
+
selectedLevelId: LevelNode['id'] | null,
|
|
85
|
+
x: number,
|
|
86
|
+
z: number,
|
|
87
|
+
rotation: number,
|
|
88
|
+
onPlaced: ElevatorToolProps['onPlaced'],
|
|
89
|
+
): void {
|
|
90
|
+
const { createNode, nodes } = useScene.getState()
|
|
91
|
+
const elevatorCount = Object.values(nodes).filter((node) => node.type === 'elevator').length
|
|
92
|
+
const serviceRange = resolveDefaultServiceRange(buildingId, selectedLevelId)
|
|
93
|
+
const supportY = resolveElevatorSupportY({
|
|
94
|
+
buildingId,
|
|
95
|
+
preferredLevelId: serviceRange.fromLevelId ?? serviceRange.defaultLevelId,
|
|
96
|
+
x,
|
|
97
|
+
z,
|
|
98
|
+
})
|
|
99
|
+
const elevator = ElevatorNode.parse({
|
|
100
|
+
name: `Elevator ${elevatorCount + 1}`,
|
|
101
|
+
parentId: buildingId,
|
|
102
|
+
position: [x, supportY, z],
|
|
103
|
+
rotation,
|
|
104
|
+
width: DEFAULT_ELEVATOR_WIDTH,
|
|
105
|
+
depth: DEFAULT_ELEVATOR_DEPTH,
|
|
106
|
+
cabHeight: DEFAULT_ELEVATOR_CAB_HEIGHT,
|
|
107
|
+
doorWidth: DEFAULT_ELEVATOR_DOOR_WIDTH,
|
|
108
|
+
doorHeight: DEFAULT_ELEVATOR_DOOR_HEIGHT,
|
|
109
|
+
...serviceRange,
|
|
110
|
+
speed: DEFAULT_ELEVATOR_SPEED,
|
|
111
|
+
doorDurationMs: DEFAULT_ELEVATOR_DOOR_DURATION_MS,
|
|
112
|
+
dwellMs: DEFAULT_ELEVATOR_DWELL_MS,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
createNode(elevator, buildingId)
|
|
116
|
+
onPlaced?.(elevator.id as AnyNodeId, buildingId)
|
|
117
|
+
sfxEmitter.emit('sfx:structure-build')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const ElevatorTool: React.FC<ElevatorToolProps> = ({ buildingId, levelId, onPlaced }) => {
|
|
121
|
+
const cursorRef = useRef<THREE.Group>(null)
|
|
122
|
+
const previewRef = useRef<THREE.Group>(null)
|
|
123
|
+
const rotationRef = useRef(0)
|
|
124
|
+
const previousGridPosRef = useRef<[number, number] | null>(null)
|
|
125
|
+
const previewGeometry = useMemo(() => createElevatorPreviewGeometry(), [])
|
|
126
|
+
const previewEdgeGeometry = useMemo(() => createElevatorPreviewEdgeGeometry(), [])
|
|
127
|
+
|
|
128
|
+
// Default-shaped elevator for the 2D floor-plan placement ghost. The 3D
|
|
129
|
+
// preview meshes below are hidden in 2D (canvas `display:none`), so this
|
|
130
|
+
// feeds `usePlacementPreview` → `FloorplanPlacementPreviewLayer`, which
|
|
131
|
+
// renders the elevator's footprint following the cursor.
|
|
132
|
+
const floorplanPreviewNode = useMemo(
|
|
133
|
+
() =>
|
|
134
|
+
ElevatorNode.parse({
|
|
135
|
+
name: 'Elevator',
|
|
136
|
+
position: [0, 0, 0],
|
|
137
|
+
rotation: 0,
|
|
138
|
+
width: DEFAULT_ELEVATOR_WIDTH,
|
|
139
|
+
depth: DEFAULT_ELEVATOR_DEPTH,
|
|
140
|
+
cabHeight: DEFAULT_ELEVATOR_CAB_HEIGHT,
|
|
141
|
+
doorWidth: DEFAULT_ELEVATOR_DOOR_WIDTH,
|
|
142
|
+
doorHeight: DEFAULT_ELEVATOR_DOOR_HEIGHT,
|
|
143
|
+
}),
|
|
144
|
+
[],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
const currentBuildingId = resolveCurrentBuildingId({
|
|
149
|
+
buildingId,
|
|
150
|
+
levelId,
|
|
151
|
+
nodes: useScene.getState().nodes,
|
|
152
|
+
})
|
|
153
|
+
if (!currentBuildingId) return
|
|
154
|
+
|
|
155
|
+
rotationRef.current = 0
|
|
156
|
+
if (previewRef.current) previewRef.current.rotation.y = 0
|
|
157
|
+
|
|
158
|
+
// Alignment candidates — anchors of every alignable object; refreshed
|
|
159
|
+
// after each placement. The elevator aligns by its ORIGIN point.
|
|
160
|
+
let alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', levelId)
|
|
161
|
+
// Snap the elevator origin onto another object's nearest real anchor and
|
|
162
|
+
// publish the guide. The probe is the RAW cursor, NOT the 0.5m-grid-snapped
|
|
163
|
+
// point: resolving against the grid point would only ever catch anchors
|
|
164
|
+
// that happen to sit on a grid line, so off-grid items (furniture, angled
|
|
165
|
+
// walls) would never surface a guide. The matched axis locks exactly to the
|
|
166
|
+
// candidate's coordinate; the other axis keeps its grid snap. Alt bypasses.
|
|
167
|
+
const alignPoint = (
|
|
168
|
+
gridX: number,
|
|
169
|
+
gridZ: number,
|
|
170
|
+
rawX: number,
|
|
171
|
+
rawZ: number,
|
|
172
|
+
bypass: boolean,
|
|
173
|
+
): [number, number] => {
|
|
174
|
+
if (bypass || alignmentCandidates.length === 0) {
|
|
175
|
+
useAlignmentGuides.getState().clear()
|
|
176
|
+
return [gridX, gridZ]
|
|
177
|
+
}
|
|
178
|
+
const ar = resolveAlignment({
|
|
179
|
+
moving: [{ nodeId: '__elevator-draft__', kind: 'corner', x: rawX, z: rawZ }],
|
|
180
|
+
candidates: alignmentCandidates,
|
|
181
|
+
threshold: ALIGNMENT_THRESHOLD_M,
|
|
182
|
+
})
|
|
183
|
+
if (ar.guides.length === 0) {
|
|
184
|
+
useAlignmentGuides.getState().clear()
|
|
185
|
+
return [gridX, gridZ]
|
|
186
|
+
}
|
|
187
|
+
useAlignmentGuides.getState().set(ar.guides)
|
|
188
|
+
let x = gridX
|
|
189
|
+
let z = gridZ
|
|
190
|
+
for (const guide of ar.guides) {
|
|
191
|
+
if (guide.axis === 'x') x = guide.coord
|
|
192
|
+
else z = guide.coord
|
|
193
|
+
}
|
|
194
|
+
return [x, z]
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const onGridMove = (event: GridEvent) => {
|
|
198
|
+
const [gridX, gridZ] = alignPoint(
|
|
199
|
+
Math.round(event.localPosition[0] * 2) / 2,
|
|
200
|
+
Math.round(event.localPosition[2] * 2) / 2,
|
|
201
|
+
event.localPosition[0],
|
|
202
|
+
event.localPosition[2],
|
|
203
|
+
event.nativeEvent?.altKey === true,
|
|
204
|
+
)
|
|
205
|
+
const supportY = resolveElevatorSupportY({
|
|
206
|
+
buildingId: currentBuildingId,
|
|
207
|
+
preferredLevelId: levelId as LevelNode['id'] | null,
|
|
208
|
+
x: gridX,
|
|
209
|
+
z: gridZ,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
cursorRef.current?.position.set(gridX, supportY + GRID_OFFSET, gridZ)
|
|
213
|
+
previewRef.current?.position.set(gridX, supportY + DEFAULT_ELEVATOR_CAB_HEIGHT / 2, gridZ)
|
|
214
|
+
|
|
215
|
+
// Publish the 2D floor-plan ghost at the snapped/aligned cursor.
|
|
216
|
+
usePlacementPreview.getState().set({
|
|
217
|
+
...floorplanPreviewNode,
|
|
218
|
+
position: [gridX, supportY, gridZ],
|
|
219
|
+
rotation: rotationRef.current,
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
if (
|
|
223
|
+
previousGridPosRef.current &&
|
|
224
|
+
(gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
|
|
225
|
+
) {
|
|
226
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
previousGridPosRef.current = [gridX, gridZ]
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const onGridClick = (event: GridEvent) => {
|
|
233
|
+
const latestBuildingId = resolveCurrentBuildingId({
|
|
234
|
+
buildingId,
|
|
235
|
+
levelId,
|
|
236
|
+
nodes: useScene.getState().nodes,
|
|
237
|
+
})
|
|
238
|
+
if (!latestBuildingId) return
|
|
239
|
+
|
|
240
|
+
const [gridX, gridZ] = alignPoint(
|
|
241
|
+
Math.round(event.localPosition[0] * 2) / 2,
|
|
242
|
+
Math.round(event.localPosition[2] * 2) / 2,
|
|
243
|
+
event.localPosition[0],
|
|
244
|
+
event.localPosition[2],
|
|
245
|
+
event.nativeEvent?.altKey === true,
|
|
246
|
+
)
|
|
247
|
+
commitElevatorPlacement(
|
|
248
|
+
latestBuildingId,
|
|
249
|
+
levelId,
|
|
250
|
+
gridX,
|
|
251
|
+
gridZ,
|
|
252
|
+
rotationRef.current,
|
|
253
|
+
onPlaced,
|
|
254
|
+
)
|
|
255
|
+
alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', levelId)
|
|
256
|
+
useAlignmentGuides.getState().clear()
|
|
257
|
+
// The placed elevator's footprint now renders for real; drop the ghost
|
|
258
|
+
// (the next grid:move re-publishes it for the following placement).
|
|
259
|
+
usePlacementPreview.getState().clear()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
263
|
+
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const ROTATION_STEP = Math.PI / 4
|
|
268
|
+
let rotationDelta = 0
|
|
269
|
+
if (event.key === 'r' || event.key === 'R') rotationDelta = ROTATION_STEP
|
|
270
|
+
else if (event.key === 't' || event.key === 'T') rotationDelta = -ROTATION_STEP
|
|
271
|
+
|
|
272
|
+
if (rotationDelta !== 0) {
|
|
273
|
+
event.preventDefault()
|
|
274
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
275
|
+
rotationRef.current += rotationDelta
|
|
276
|
+
if (previewRef.current) previewRef.current.rotation.y = rotationRef.current
|
|
277
|
+
// Reflect the rotation in the 2D ghost immediately (no pointer move
|
|
278
|
+
// needed) by republishing at the last snapped cursor position.
|
|
279
|
+
const last = previousGridPosRef.current
|
|
280
|
+
if (last) {
|
|
281
|
+
usePlacementPreview.getState().set({
|
|
282
|
+
...floorplanPreviewNode,
|
|
283
|
+
position: [last[0], 0, last[1]],
|
|
284
|
+
rotation: rotationRef.current,
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
emitter.on('grid:move', onGridMove)
|
|
291
|
+
emitter.on('grid:click', onGridClick)
|
|
292
|
+
window.addEventListener('keydown', onKeyDown)
|
|
293
|
+
|
|
294
|
+
return () => {
|
|
295
|
+
emitter.off('grid:move', onGridMove)
|
|
296
|
+
emitter.off('grid:click', onGridClick)
|
|
297
|
+
window.removeEventListener('keydown', onKeyDown)
|
|
298
|
+
useAlignmentGuides.getState().clear()
|
|
299
|
+
usePlacementPreview.getState().clear()
|
|
300
|
+
}
|
|
301
|
+
}, [buildingId, levelId, onPlaced, floorplanPreviewNode])
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<group>
|
|
305
|
+
<CursorSphere ref={cursorRef} />
|
|
306
|
+
<group ref={previewRef}>
|
|
307
|
+
<mesh geometry={previewGeometry}>
|
|
308
|
+
<meshStandardMaterial color="#38bdf8" depthWrite={false} opacity={0.12} transparent />
|
|
309
|
+
</mesh>
|
|
310
|
+
<lineSegments geometry={previewEdgeGeometry}>
|
|
311
|
+
<lineBasicMaterial color="#38bdf8" opacity={0.9} transparent />
|
|
312
|
+
</lineSegments>
|
|
313
|
+
<mesh
|
|
314
|
+
position={[0, -DEFAULT_ELEVATOR_CAB_HEIGHT / 2 + 0.015, 0]}
|
|
315
|
+
rotation={[-Math.PI / 2, 0, 0]}
|
|
316
|
+
>
|
|
317
|
+
<planeGeometry args={[DEFAULT_ELEVATOR_WIDTH, DEFAULT_ELEVATOR_DEPTH]} />
|
|
318
|
+
<meshBasicMaterial color="#38bdf8" opacity={0.16} transparent />
|
|
319
|
+
</mesh>
|
|
320
|
+
<mesh position={[0, 0, -DEFAULT_ELEVATOR_DEPTH / 2 - 0.01]}>
|
|
321
|
+
<planeGeometry args={[DEFAULT_ELEVATOR_WIDTH, DEFAULT_ELEVATOR_CAB_HEIGHT]} />
|
|
322
|
+
<meshBasicMaterial color="#e0f2fe" opacity={0.18} transparent />
|
|
323
|
+
</mesh>
|
|
324
|
+
<mesh
|
|
325
|
+
position={[
|
|
326
|
+
0,
|
|
327
|
+
-DEFAULT_ELEVATOR_CAB_HEIGHT / 2 + DEFAULT_ELEVATOR_DOOR_HEIGHT / 2,
|
|
328
|
+
-DEFAULT_ELEVATOR_DEPTH / 2 - 0.03,
|
|
329
|
+
]}
|
|
330
|
+
>
|
|
331
|
+
<boxGeometry args={[DEFAULT_ELEVATOR_DOOR_WIDTH, DEFAULT_ELEVATOR_DOOR_HEIGHT, 0.035]} />
|
|
332
|
+
<meshStandardMaterial color="#e5e7eb" depthWrite={false} opacity={0.45} transparent />
|
|
333
|
+
</mesh>
|
|
334
|
+
<mesh
|
|
335
|
+
position={[
|
|
336
|
+
0,
|
|
337
|
+
-DEFAULT_ELEVATOR_CAB_HEIGHT / 2 + DEFAULT_ELEVATOR_DOOR_HEIGHT / 2,
|
|
338
|
+
-DEFAULT_ELEVATOR_DEPTH / 2 - 0.049,
|
|
339
|
+
]}
|
|
340
|
+
>
|
|
341
|
+
<boxGeometry
|
|
342
|
+
args={[DEFAULT_ELEVATOR_DOOR_WIDTH + 0.14, DEFAULT_ELEVATOR_DOOR_HEIGHT + 0.14, 0.01]}
|
|
343
|
+
/>
|
|
344
|
+
<meshBasicMaterial color="#38bdf8" opacity={0.5} transparent />
|
|
345
|
+
</mesh>
|
|
346
|
+
</group>
|
|
347
|
+
</group>
|
|
348
|
+
)
|
|
349
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNodeId,
|
|
3
|
+
type BuildingNode,
|
|
4
|
+
type ElevatorNode,
|
|
5
|
+
ElevatorNode as ElevatorNodeSchema,
|
|
6
|
+
emitter,
|
|
7
|
+
type GridEvent,
|
|
8
|
+
type LevelNode,
|
|
9
|
+
pauseSceneHistory,
|
|
10
|
+
resumeSceneHistory,
|
|
11
|
+
sceneRegistry,
|
|
12
|
+
useLiveTransforms,
|
|
13
|
+
useScene,
|
|
14
|
+
} from '@pascal-app/core'
|
|
15
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
16
|
+
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
17
|
+
import { resolveElevatorSupportY } from '../../../lib/elevator-support'
|
|
18
|
+
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
19
|
+
import useEditor from '../../../store/use-editor'
|
|
20
|
+
import { CursorSphere } from '../shared/cursor-sphere'
|
|
21
|
+
|
|
22
|
+
function stripMoveMetadata(metadata: ElevatorNode['metadata']) {
|
|
23
|
+
if (typeof metadata !== 'object' || metadata === null || Array.isArray(metadata)) {
|
|
24
|
+
return metadata
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const nextMeta = { ...(metadata as Record<string, unknown>) }
|
|
28
|
+
delete nextMeta.isNew
|
|
29
|
+
delete nextMeta.isTransient
|
|
30
|
+
return nextMeta as ElevatorNode['metadata']
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function MoveElevatorTool({
|
|
34
|
+
node: movingNode,
|
|
35
|
+
onCommitted,
|
|
36
|
+
}: {
|
|
37
|
+
node: ElevatorNode
|
|
38
|
+
onCommitted?: (nodeId: AnyNodeId) => void
|
|
39
|
+
}) {
|
|
40
|
+
const onCommittedRef = useRef(onCommitted)
|
|
41
|
+
const historyPausedRef = useRef(false)
|
|
42
|
+
const previousGridPosRef = useRef<[number, number] | null>(null)
|
|
43
|
+
const dragAnchorRef = useRef<[number, number] | null>(null)
|
|
44
|
+
const previewPositionRef = useRef<ElevatorNode['position']>([
|
|
45
|
+
movingNode.position[0],
|
|
46
|
+
movingNode.position[1],
|
|
47
|
+
movingNode.position[2],
|
|
48
|
+
])
|
|
49
|
+
const [cursorPosition, setCursorPosition] = useState<[number, number, number]>(() => [
|
|
50
|
+
movingNode.position[0],
|
|
51
|
+
movingNode.position[1],
|
|
52
|
+
movingNode.position[2],
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
const exitMoveMode = useCallback(() => {
|
|
56
|
+
useEditor.getState().setMovingNode(null)
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
onCommittedRef.current = onCommitted
|
|
61
|
+
}, [onCommitted])
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const pauseHistory = () => {
|
|
65
|
+
const temporal = useScene.temporal.getState()
|
|
66
|
+
if (historyPausedRef.current || !temporal.isTracking) return
|
|
67
|
+
pauseSceneHistory(useScene)
|
|
68
|
+
historyPausedRef.current = true
|
|
69
|
+
}
|
|
70
|
+
const resumeHistory = () => {
|
|
71
|
+
if (!historyPausedRef.current) return
|
|
72
|
+
resumeSceneHistory(useScene)
|
|
73
|
+
historyPausedRef.current = false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pauseHistory()
|
|
77
|
+
dragAnchorRef.current = null
|
|
78
|
+
previousGridPosRef.current = null
|
|
79
|
+
const movingNodeId = (movingNode as { id?: ElevatorNode['id'] }).id
|
|
80
|
+
|
|
81
|
+
const meta =
|
|
82
|
+
typeof movingNode.metadata === 'object' && movingNode.metadata !== null
|
|
83
|
+
? (movingNode.metadata as Record<string, unknown>)
|
|
84
|
+
: {}
|
|
85
|
+
const isNew = !!meta.isNew
|
|
86
|
+
const committedMeta = stripMoveMetadata(movingNode.metadata)
|
|
87
|
+
const original = {
|
|
88
|
+
position: [...movingNode.position] as ElevatorNode['position'],
|
|
89
|
+
rotation: movingNode.rotation,
|
|
90
|
+
metadata: movingNode.metadata,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let wasCommitted = false
|
|
94
|
+
let wasCancelled = false
|
|
95
|
+
let pendingRotation = movingNode.rotation
|
|
96
|
+
const supportBuildingId = movingNode.parentId as BuildingNode['id'] | null | undefined
|
|
97
|
+
const supportLevelId = (movingNode.fromLevelId ?? movingNode.defaultLevelId) as
|
|
98
|
+
| LevelNode['id']
|
|
99
|
+
| null
|
|
100
|
+
|
|
101
|
+
const applyPreview = (
|
|
102
|
+
position: ElevatorNode['position'],
|
|
103
|
+
rotation: ElevatorNode['rotation'],
|
|
104
|
+
) => {
|
|
105
|
+
if (movingNodeId) {
|
|
106
|
+
useLiveTransforms.getState().set(movingNodeId, { position, rotation })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const object = movingNodeId ? sceneRegistry.nodes.get(movingNodeId) : null
|
|
110
|
+
if (object) {
|
|
111
|
+
object.position.set(position[0], position[1], position[2])
|
|
112
|
+
object.rotation.y = rotation
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const resetObject = (
|
|
117
|
+
position: ElevatorNode['position'],
|
|
118
|
+
rotation: ElevatorNode['rotation'],
|
|
119
|
+
) => {
|
|
120
|
+
const object = movingNodeId ? sceneRegistry.nodes.get(movingNodeId) : null
|
|
121
|
+
if (object) {
|
|
122
|
+
object.position.set(position[0], position[1], position[2])
|
|
123
|
+
object.rotation.y = rotation
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const clearPreview = () => {
|
|
128
|
+
if (movingNodeId) {
|
|
129
|
+
useLiveTransforms.getState().clear(movingNodeId)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const onGridMove = (event: GridEvent) => {
|
|
134
|
+
const rawX = Math.round(event.localPosition[0] * 2) / 2
|
|
135
|
+
const rawZ = Math.round(event.localPosition[2] * 2) / 2
|
|
136
|
+
const anchor = dragAnchorRef.current ?? [rawX, rawZ]
|
|
137
|
+
dragAnchorRef.current = anchor
|
|
138
|
+
const gridX = movingNode.position[0] + (rawX - anchor[0])
|
|
139
|
+
const gridZ = movingNode.position[2] + (rawZ - anchor[1])
|
|
140
|
+
const supportY = resolveElevatorSupportY({
|
|
141
|
+
buildingId: supportBuildingId,
|
|
142
|
+
preferredLevelId: supportLevelId,
|
|
143
|
+
x: gridX,
|
|
144
|
+
z: gridZ,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
previousGridPosRef.current &&
|
|
149
|
+
(gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
|
|
150
|
+
) {
|
|
151
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
previousGridPosRef.current = [gridX, gridZ]
|
|
155
|
+
setCursorPosition([gridX, supportY, gridZ])
|
|
156
|
+
previewPositionRef.current = [gridX, supportY, gridZ]
|
|
157
|
+
applyPreview(previewPositionRef.current, pendingRotation)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const onGridClick = (event: GridEvent) => {
|
|
161
|
+
const nextPosition: ElevatorNode['position'] = [...previewPositionRef.current]
|
|
162
|
+
|
|
163
|
+
wasCommitted = true
|
|
164
|
+
clearPreview()
|
|
165
|
+
resumeHistory()
|
|
166
|
+
if (movingNodeId && useScene.getState().nodes[movingNodeId as AnyNodeId]) {
|
|
167
|
+
useScene.getState().updateNode(movingNodeId as AnyNodeId, {
|
|
168
|
+
position: nextPosition,
|
|
169
|
+
rotation: pendingRotation,
|
|
170
|
+
metadata: committedMeta,
|
|
171
|
+
})
|
|
172
|
+
onCommittedRef.current?.(movingNodeId as AnyNodeId)
|
|
173
|
+
} else if (movingNode.parentId) {
|
|
174
|
+
const elevator = ElevatorNodeSchema.parse({
|
|
175
|
+
...movingNode,
|
|
176
|
+
id: undefined,
|
|
177
|
+
position: nextPosition,
|
|
178
|
+
rotation: pendingRotation,
|
|
179
|
+
metadata: committedMeta,
|
|
180
|
+
})
|
|
181
|
+
useScene.getState().createNode(elevator, movingNode.parentId as AnyNodeId)
|
|
182
|
+
onCommittedRef.current?.(elevator.id as AnyNodeId)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
sfxEmitter.emit('sfx:item-place')
|
|
186
|
+
exitMoveMode()
|
|
187
|
+
event.nativeEvent?.stopPropagation?.()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const onCancel = () => {
|
|
191
|
+
wasCancelled = true
|
|
192
|
+
clearPreview()
|
|
193
|
+
if (isNew && movingNodeId) {
|
|
194
|
+
useScene.getState().deleteNode(movingNodeId as AnyNodeId)
|
|
195
|
+
} else {
|
|
196
|
+
if (movingNodeId) {
|
|
197
|
+
useScene.getState().updateNode(movingNodeId as AnyNodeId, {
|
|
198
|
+
position: original.position,
|
|
199
|
+
rotation: original.rotation,
|
|
200
|
+
metadata: original.metadata,
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
resetObject(original.position, original.rotation)
|
|
205
|
+
resumeHistory()
|
|
206
|
+
markToolCancelConsumed()
|
|
207
|
+
exitMoveMode()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
211
|
+
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const ROTATION_STEP = Math.PI / 4
|
|
216
|
+
let rotationDelta = 0
|
|
217
|
+
if (event.key === 'r' || event.key === 'R') rotationDelta = ROTATION_STEP
|
|
218
|
+
else if (event.key === 't' || event.key === 'T') rotationDelta = -ROTATION_STEP
|
|
219
|
+
|
|
220
|
+
if (rotationDelta !== 0) {
|
|
221
|
+
event.preventDefault()
|
|
222
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
223
|
+
pendingRotation += rotationDelta
|
|
224
|
+
applyPreview(previewPositionRef.current, pendingRotation)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
emitter.on('grid:move', onGridMove)
|
|
229
|
+
emitter.on('grid:click', onGridClick)
|
|
230
|
+
emitter.on('tool:cancel', onCancel)
|
|
231
|
+
window.addEventListener('keydown', onKeyDown)
|
|
232
|
+
|
|
233
|
+
return () => {
|
|
234
|
+
clearPreview()
|
|
235
|
+
if (!(wasCommitted || wasCancelled || isNew) && movingNodeId) {
|
|
236
|
+
useScene.getState().updateNode(movingNodeId as AnyNodeId, {
|
|
237
|
+
position: original.position,
|
|
238
|
+
rotation: original.rotation,
|
|
239
|
+
metadata: original.metadata,
|
|
240
|
+
})
|
|
241
|
+
resetObject(original.position, original.rotation)
|
|
242
|
+
}
|
|
243
|
+
resumeHistory()
|
|
244
|
+
emitter.off('grid:move', onGridMove)
|
|
245
|
+
emitter.off('grid:click', onGridClick)
|
|
246
|
+
emitter.off('tool:cancel', onCancel)
|
|
247
|
+
window.removeEventListener('keydown', onKeyDown)
|
|
248
|
+
}
|
|
249
|
+
}, [movingNode, exitMoveMode])
|
|
250
|
+
|
|
251
|
+
return <CursorSphere position={cursorPosition} showTooltip={false} />
|
|
252
|
+
}
|