@pascal-app/editor 0.7.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 +12 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +755 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +62 -54
- 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 +203 -87
- package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
- package/src/components/editor/first-person-controls.tsx +836 -31
- 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 +5837 -12006
- 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 +234 -190
- 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 +435 -158
- 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 +539 -0
- package/src/components/editor/thumbnail-generator.tsx +27 -64
- package/src/components/editor/use-floorplan-background-placement.ts +122 -35
- 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-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
- 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 +29 -14
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
- 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 +2 -2
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- 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 +40 -107
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +33 -9
- package/src/components/tools/item/placement-strategies.ts +201 -25
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
- 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 -543
- 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 +57 -1
- 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 +36 -11
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +5 -76
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +33 -196
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +262 -37
- package/src/components/ui/command-palette/editor-commands.tsx +24 -17
- package/src/components/ui/command-palette/index.tsx +6 -256
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +17 -35
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +48 -11
- package/src/components/ui/helpers/helper-manager.tsx +21 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
- package/src/components/ui/level-duplicate-dialog.tsx +5 -6
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +33 -74
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
- 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 +8 -12
- 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 +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/ui/slider.tsx +1 -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 +24 -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 +125 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +243 -4
- 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.test.ts +0 -2
- package/src/lib/level-duplication.ts +55 -43
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +212 -20
- 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 +2 -2
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +25 -1
- package/src/lib/sfx-player.ts +75 -27
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-duplication.ts +2 -2
- 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 +342 -91
- 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/tsconfig.json +2 -1
- 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/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- 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 -257
- 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 -179
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
- package/src/components/tools/fence/move-fence-tool.tsx +0 -298
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -359
- 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 -176
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
- package/src/components/tools/wall/move-wall-tool.tsx +0 -356
- 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 -250
- package/src/components/ui/panels/column-panel.tsx +0 -715
- package/src/components/ui/panels/door-panel.tsx +0 -1292
- package/src/components/ui/panels/fence-panel.tsx +0 -227
- 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 -230
- 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 -155
- package/src/components/ui/panels/stair-panel.tsx +0 -525
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -177
- package/src/components/ui/panels/window-panel.tsx +0 -960
- package/src/components/ui/viewer-toolbar.tsx +0 -436
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
type AnyNodeId,
|
|
5
|
-
emitter,
|
|
6
|
-
type FenceNode,
|
|
7
|
-
type GridEvent,
|
|
8
|
-
type LevelNode,
|
|
9
|
-
sceneRegistry,
|
|
10
|
-
useLiveTransforms,
|
|
11
|
-
useScene,
|
|
12
|
-
type WallNode,
|
|
13
|
-
} from '@pascal-app/core'
|
|
14
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
15
|
-
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
16
|
-
import type * as THREE from 'three'
|
|
17
|
-
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
18
|
-
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
19
|
-
import useEditor from '../../../store/use-editor'
|
|
20
|
-
import { snapFenceDraftPoint } from './fence-drafting'
|
|
21
|
-
import { CursorSphere } from '../shared/cursor-sphere'
|
|
22
|
-
|
|
23
|
-
function samePoint(a: [number, number], b: [number, number]) {
|
|
24
|
-
return a[0] === b[0] && a[1] === b[1]
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
type LinkedFenceSnapshot = {
|
|
28
|
-
id: FenceNode['id']
|
|
29
|
-
start: [number, number]
|
|
30
|
-
end: [number, number]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function getLinkedFenceSnapshots(args: {
|
|
34
|
-
fenceId: FenceNode['id']
|
|
35
|
-
fenceParentId: string | null
|
|
36
|
-
originalStart: [number, number]
|
|
37
|
-
originalEnd: [number, number]
|
|
38
|
-
}) {
|
|
39
|
-
const { fenceId, fenceParentId, originalStart, originalEnd } = args
|
|
40
|
-
const { nodes } = useScene.getState()
|
|
41
|
-
const snapshots: LinkedFenceSnapshot[] = []
|
|
42
|
-
|
|
43
|
-
for (const node of Object.values(nodes)) {
|
|
44
|
-
if (!(node?.type === 'fence' && node.id !== fenceId)) {
|
|
45
|
-
continue
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if ((node.parentId ?? null) !== fenceParentId) {
|
|
49
|
-
continue
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
!samePoint(node.start, originalStart) &&
|
|
54
|
-
!samePoint(node.start, originalEnd) &&
|
|
55
|
-
!samePoint(node.end, originalStart) &&
|
|
56
|
-
!samePoint(node.end, originalEnd)
|
|
57
|
-
) {
|
|
58
|
-
continue
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
snapshots.push({
|
|
62
|
-
id: node.id,
|
|
63
|
-
start: [...node.start] as [number, number],
|
|
64
|
-
end: [...node.end] as [number, number],
|
|
65
|
-
})
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return snapshots
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function getLinkedFenceUpdates(
|
|
72
|
-
linkedFences: LinkedFenceSnapshot[],
|
|
73
|
-
originalStart: [number, number],
|
|
74
|
-
originalEnd: [number, number],
|
|
75
|
-
nextStart: [number, number],
|
|
76
|
-
nextEnd: [number, number],
|
|
77
|
-
) {
|
|
78
|
-
return linkedFences.map((fence) => ({
|
|
79
|
-
id: fence.id,
|
|
80
|
-
start: samePoint(fence.start, originalStart)
|
|
81
|
-
? nextStart
|
|
82
|
-
: samePoint(fence.start, originalEnd)
|
|
83
|
-
? nextEnd
|
|
84
|
-
: fence.start,
|
|
85
|
-
end: samePoint(fence.end, originalStart)
|
|
86
|
-
? nextStart
|
|
87
|
-
: samePoint(fence.end, originalEnd)
|
|
88
|
-
? nextEnd
|
|
89
|
-
: fence.end,
|
|
90
|
-
}))
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export const MoveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => {
|
|
94
|
-
const activatedAtRef = useRef<number>(Date.now())
|
|
95
|
-
const previousGridPosRef = useRef<[number, number] | null>(null)
|
|
96
|
-
const originalStartRef = useRef<[number, number]>([...node.start] as [number, number])
|
|
97
|
-
const originalEndRef = useRef<[number, number]>([...node.end] as [number, number])
|
|
98
|
-
const linkedOriginalsRef = useRef(
|
|
99
|
-
getLinkedFenceSnapshots({
|
|
100
|
-
fenceId: node.id,
|
|
101
|
-
fenceParentId: node.parentId ?? null,
|
|
102
|
-
originalStart: node.start,
|
|
103
|
-
originalEnd: node.end,
|
|
104
|
-
}),
|
|
105
|
-
)
|
|
106
|
-
const dragAnchorRef = useRef<[number, number] | null>(null)
|
|
107
|
-
const nodeIdRef = useRef(node.id)
|
|
108
|
-
const previewRef = useRef<{ start: [number, number]; end: [number, number] } | null>(null)
|
|
109
|
-
|
|
110
|
-
const [cursorLocalPos, setCursorLocalPos] = useState<[number, number, number]>(() => {
|
|
111
|
-
const centerX = (node.start[0] + node.end[0]) / 2
|
|
112
|
-
const centerZ = (node.start[1] + node.end[1]) / 2
|
|
113
|
-
return [centerX, 0, centerZ]
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
const exitMoveMode = useCallback(() => {
|
|
117
|
-
useEditor.getState().setMovingNode(null)
|
|
118
|
-
}, [])
|
|
119
|
-
|
|
120
|
-
useEffect(() => {
|
|
121
|
-
const nodeId = nodeIdRef.current
|
|
122
|
-
const originalStart = originalStartRef.current
|
|
123
|
-
const originalEnd = originalEndRef.current
|
|
124
|
-
const levelNode =
|
|
125
|
-
node.parentId && useScene.getState().nodes[node.parentId as AnyNodeId]?.type === 'level'
|
|
126
|
-
? (useScene.getState().nodes[node.parentId as AnyNodeId] as LevelNode)
|
|
127
|
-
: null
|
|
128
|
-
const levelChildren = levelNode?.children ?? []
|
|
129
|
-
const levelWalls = levelChildren
|
|
130
|
-
.map((childId) => useScene.getState().nodes[childId as AnyNodeId])
|
|
131
|
-
.filter((child): child is WallNode => child?.type === 'wall')
|
|
132
|
-
const levelFences = levelChildren
|
|
133
|
-
.map((childId) => useScene.getState().nodes[childId as AnyNodeId])
|
|
134
|
-
.filter((child): child is FenceNode => child?.type === 'fence')
|
|
135
|
-
|
|
136
|
-
useScene.temporal.getState().pause()
|
|
137
|
-
let wasCommitted = false
|
|
138
|
-
|
|
139
|
-
const setMeshOffset = (fenceId: FenceNode['id'], deltaX: number, deltaZ: number) => {
|
|
140
|
-
const mesh = sceneRegistry.nodes.get(fenceId) as THREE.Object3D | undefined
|
|
141
|
-
if (!mesh) {
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
mesh.position.set(deltaX, 0, deltaZ)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const setFenceLiveTransform = (fence: FenceNode, deltaX: number, deltaZ: number) => {
|
|
149
|
-
const originalCenterX = (fence.start[0] + fence.end[0]) / 2
|
|
150
|
-
const originalCenterZ = (fence.start[1] + fence.end[1]) / 2
|
|
151
|
-
useLiveTransforms.getState().set(fence.id, {
|
|
152
|
-
position: [originalCenterX + deltaX, 0, originalCenterZ + deltaZ],
|
|
153
|
-
rotation: 0,
|
|
154
|
-
})
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const clearPreviewState = () => {
|
|
158
|
-
setMeshOffset(nodeId, 0, 0)
|
|
159
|
-
useLiveTransforms.getState().clear(nodeId)
|
|
160
|
-
|
|
161
|
-
for (const linkedFence of linkedOriginalsRef.current) {
|
|
162
|
-
setMeshOffset(linkedFence.id, 0, 0)
|
|
163
|
-
useLiveTransforms.getState().clear(linkedFence.id)
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const applyNodePreview = (updates: Array<{ id: FenceNode['id']; start: [number, number]; end: [number, number] }>) => {
|
|
168
|
-
useScene.getState().updateNodes(
|
|
169
|
-
updates.map((entry) => ({
|
|
170
|
-
id: entry.id as AnyNodeId,
|
|
171
|
-
data: { start: entry.start, end: entry.end },
|
|
172
|
-
})),
|
|
173
|
-
)
|
|
174
|
-
for (const entry of updates) {
|
|
175
|
-
useScene.getState().markDirty(entry.id as AnyNodeId)
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const applyPreview = (nextStart: [number, number], nextEnd: [number, number]) => {
|
|
180
|
-
previewRef.current = { start: nextStart, end: nextEnd }
|
|
181
|
-
const centerX = (nextStart[0] + nextEnd[0]) / 2
|
|
182
|
-
const centerZ = (nextStart[1] + nextEnd[1]) / 2
|
|
183
|
-
setCursorLocalPos([centerX, 0, centerZ])
|
|
184
|
-
const deltaX = nextStart[0] - originalStart[0]
|
|
185
|
-
const deltaZ = nextStart[1] - originalStart[1]
|
|
186
|
-
setMeshOffset(nodeId, deltaX, deltaZ)
|
|
187
|
-
setFenceLiveTransform(node, deltaX, deltaZ)
|
|
188
|
-
|
|
189
|
-
for (const linkedFence of linkedOriginalsRef.current) {
|
|
190
|
-
setMeshOffset(linkedFence.id, deltaX, deltaZ)
|
|
191
|
-
setFenceLiveTransform(
|
|
192
|
-
{
|
|
193
|
-
...node,
|
|
194
|
-
id: linkedFence.id,
|
|
195
|
-
start: linkedFence.start,
|
|
196
|
-
end: linkedFence.end,
|
|
197
|
-
},
|
|
198
|
-
deltaX,
|
|
199
|
-
deltaZ,
|
|
200
|
-
)
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const onGridMove = (event: GridEvent) => {
|
|
205
|
-
const [localX, localZ] = snapFenceDraftPoint({
|
|
206
|
-
point: [event.localPosition[0], event.localPosition[2]],
|
|
207
|
-
walls: levelWalls,
|
|
208
|
-
fences: levelFences,
|
|
209
|
-
ignoreFenceIds: [nodeId],
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
previousGridPosRef.current &&
|
|
214
|
-
(localX !== previousGridPosRef.current[0] || localZ !== previousGridPosRef.current[1])
|
|
215
|
-
) {
|
|
216
|
-
sfxEmitter.emit('sfx:grid-snap')
|
|
217
|
-
}
|
|
218
|
-
previousGridPosRef.current = [localX, localZ]
|
|
219
|
-
|
|
220
|
-
const anchor = dragAnchorRef.current ?? [localX, localZ]
|
|
221
|
-
dragAnchorRef.current = anchor
|
|
222
|
-
|
|
223
|
-
const deltaX = localX - anchor[0]
|
|
224
|
-
const deltaZ = localZ - anchor[1]
|
|
225
|
-
|
|
226
|
-
const nextStart: [number, number] = [originalStart[0] + deltaX, originalStart[1] + deltaZ]
|
|
227
|
-
const nextEnd: [number, number] = [originalEnd[0] + deltaX, originalEnd[1] + deltaZ]
|
|
228
|
-
|
|
229
|
-
applyPreview(nextStart, nextEnd)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const onGridClick = (event: GridEvent) => {
|
|
233
|
-
if (Date.now() - activatedAtRef.current < 150) {
|
|
234
|
-
event.nativeEvent?.stopPropagation?.()
|
|
235
|
-
return
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
|
|
239
|
-
|
|
240
|
-
wasCommitted = true
|
|
241
|
-
|
|
242
|
-
useScene.temporal.getState().resume()
|
|
243
|
-
applyNodePreview([
|
|
244
|
-
{ id: nodeId, start: preview.start, end: preview.end },
|
|
245
|
-
...getLinkedFenceUpdates(
|
|
246
|
-
linkedOriginalsRef.current,
|
|
247
|
-
originalStart,
|
|
248
|
-
originalEnd,
|
|
249
|
-
preview.start,
|
|
250
|
-
preview.end,
|
|
251
|
-
),
|
|
252
|
-
])
|
|
253
|
-
useLiveTransforms.getState().clear(nodeId)
|
|
254
|
-
for (const linkedFence of linkedOriginalsRef.current) {
|
|
255
|
-
useLiveTransforms.getState().clear(linkedFence.id)
|
|
256
|
-
}
|
|
257
|
-
useScene.temporal.getState().pause()
|
|
258
|
-
|
|
259
|
-
sfxEmitter.emit('sfx:item-place')
|
|
260
|
-
useViewer.getState().setSelection({ selectedIds: [nodeId] })
|
|
261
|
-
exitMoveMode()
|
|
262
|
-
event.nativeEvent?.stopPropagation?.()
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const onCancel = () => {
|
|
266
|
-
clearPreviewState()
|
|
267
|
-
useViewer.getState().setSelection({ selectedIds: [nodeId] })
|
|
268
|
-
useScene.temporal.getState().resume()
|
|
269
|
-
markToolCancelConsumed()
|
|
270
|
-
exitMoveMode()
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
emitter.on('grid:move', onGridMove)
|
|
274
|
-
emitter.on('grid:click', onGridClick)
|
|
275
|
-
emitter.on('tool:cancel', onCancel)
|
|
276
|
-
|
|
277
|
-
return () => {
|
|
278
|
-
if (!wasCommitted) {
|
|
279
|
-
clearPreviewState()
|
|
280
|
-
} else {
|
|
281
|
-
useLiveTransforms.getState().clear(nodeId)
|
|
282
|
-
for (const linkedFence of linkedOriginalsRef.current) {
|
|
283
|
-
useLiveTransforms.getState().clear(linkedFence.id)
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
useScene.temporal.getState().resume()
|
|
287
|
-
emitter.off('grid:move', onGridMove)
|
|
288
|
-
emitter.off('grid:click', onGridClick)
|
|
289
|
-
emitter.off('tool:cancel', onCancel)
|
|
290
|
-
}
|
|
291
|
-
}, [exitMoveMode, node])
|
|
292
|
-
|
|
293
|
-
return (
|
|
294
|
-
<group>
|
|
295
|
-
<CursorSphere position={cursorLocalPos} showTooltip={false} />
|
|
296
|
-
</group>
|
|
297
|
-
)
|
|
298
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
2
|
-
import useEditor from '../../../store/use-editor'
|
|
3
|
-
import { useDraftNode } from './use-draft-node'
|
|
4
|
-
import { usePlacementCoordinator } from './use-placement-coordinator'
|
|
5
|
-
|
|
6
|
-
export const ItemTool: React.FC = () => {
|
|
7
|
-
const selectedItem = useEditor((state) => state.selectedItem)
|
|
8
|
-
const draftNode = useDraftNode()
|
|
9
|
-
|
|
10
|
-
const cursor = usePlacementCoordinator({
|
|
11
|
-
asset: selectedItem,
|
|
12
|
-
draftNode,
|
|
13
|
-
initDraft: (gridPosition) => {
|
|
14
|
-
if (selectedItem && !selectedItem.attachTo) {
|
|
15
|
-
draftNode.create(gridPosition, selectedItem)
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
|
-
onCommitted: () => {
|
|
19
|
-
sfxEmitter.emit('sfx:item-place')
|
|
20
|
-
return true
|
|
21
|
-
},
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
if (!selectedItem) return null
|
|
25
|
-
return <>{cursor}</>
|
|
26
|
-
}
|
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type AnyNodeId,
|
|
3
|
-
emitter,
|
|
4
|
-
type FenceNode,
|
|
5
|
-
type GridEvent,
|
|
6
|
-
type LevelNode,
|
|
7
|
-
type RoofNode,
|
|
8
|
-
type RoofSegmentNode,
|
|
9
|
-
type StairNode,
|
|
10
|
-
type StairSegmentNode,
|
|
11
|
-
sceneRegistry,
|
|
12
|
-
useLiveTransforms,
|
|
13
|
-
useScene,
|
|
14
|
-
type WallNode,
|
|
15
|
-
} from '@pascal-app/core'
|
|
16
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
17
|
-
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
18
|
-
import * as THREE from 'three'
|
|
19
|
-
import { clearRoofDuplicateMetadata } from '../../../lib/roof-duplication'
|
|
20
|
-
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
21
|
-
import useEditor from '../../../store/use-editor'
|
|
22
|
-
import { snapFenceDraftPoint } from '../fence/fence-drafting'
|
|
23
|
-
import { CursorSphere } from '../shared/cursor-sphere'
|
|
24
|
-
import type { WallPlanPoint } from '../wall/wall-drafting'
|
|
25
|
-
|
|
26
|
-
export const MoveRoofTool: React.FC<{
|
|
27
|
-
node: RoofNode | RoofSegmentNode | StairNode | StairSegmentNode
|
|
28
|
-
}> = ({ node: movingNode }) => {
|
|
29
|
-
const exitMoveMode = useCallback(() => {
|
|
30
|
-
useEditor.getState().setMovingNode(null)
|
|
31
|
-
}, [])
|
|
32
|
-
|
|
33
|
-
const previousGridPosRef = useRef<[number, number] | null>(null)
|
|
34
|
-
|
|
35
|
-
const [cursorWorldPos, setCursorWorldPos] = useState<[number, number, number]>(() => {
|
|
36
|
-
const obj = sceneRegistry.nodes.get(movingNode.id)
|
|
37
|
-
if (obj) {
|
|
38
|
-
const worldPos = obj.getWorldPosition(new THREE.Vector3())
|
|
39
|
-
// Cursor renders inside the building-local ToolManager group, so convert
|
|
40
|
-
// world → building-local to honor any building rotation.
|
|
41
|
-
const buildingId = useViewer.getState().selection.buildingId
|
|
42
|
-
const buildingObj = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
|
|
43
|
-
if (buildingObj) buildingObj.worldToLocal(worldPos)
|
|
44
|
-
return [worldPos.x, worldPos.y, worldPos.z]
|
|
45
|
-
}
|
|
46
|
-
// Fallback if not registered (e.g. newly created duplicate without mesh yet)
|
|
47
|
-
if (
|
|
48
|
-
(movingNode.type === 'roof-segment' || movingNode.type === 'stair-segment') &&
|
|
49
|
-
movingNode.parentId
|
|
50
|
-
) {
|
|
51
|
-
const parentNode = useScene.getState().nodes[movingNode.parentId as AnyNodeId]
|
|
52
|
-
if (parentNode && 'position' in parentNode && 'rotation' in parentNode) {
|
|
53
|
-
const parentAngle = parentNode.rotation as number
|
|
54
|
-
const px = parentNode.position[0] as number
|
|
55
|
-
const py = parentNode.position[1] as number
|
|
56
|
-
const pz = parentNode.position[2] as number
|
|
57
|
-
const lx = movingNode.position[0]
|
|
58
|
-
const ly = movingNode.position[1]
|
|
59
|
-
const lz = movingNode.position[2]
|
|
60
|
-
|
|
61
|
-
const wx = lx * Math.cos(parentAngle) - lz * Math.sin(parentAngle) + px
|
|
62
|
-
const wz = lx * Math.sin(parentAngle) + lz * Math.cos(parentAngle) + pz
|
|
63
|
-
return [wx, py + ly, wz]
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return [movingNode.position[0], movingNode.position[1], movingNode.position[2]]
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
useScene.temporal.getState().pause()
|
|
71
|
-
|
|
72
|
-
const meta =
|
|
73
|
-
typeof movingNode.metadata === 'object' && movingNode.metadata !== null
|
|
74
|
-
? (movingNode.metadata as Record<string, unknown>)
|
|
75
|
-
: {}
|
|
76
|
-
const isNew = !!meta.isNew
|
|
77
|
-
const committedMeta: RoofNode['metadata'] = (() => {
|
|
78
|
-
if (
|
|
79
|
-
typeof movingNode.metadata !== 'object' ||
|
|
80
|
-
movingNode.metadata === null ||
|
|
81
|
-
Array.isArray(movingNode.metadata)
|
|
82
|
-
) {
|
|
83
|
-
return movingNode.metadata
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const nextMeta = { ...movingNode.metadata } as Record<string, unknown>
|
|
87
|
-
delete nextMeta.isNew
|
|
88
|
-
delete nextMeta.isTransient
|
|
89
|
-
return nextMeta as RoofNode['metadata']
|
|
90
|
-
})()
|
|
91
|
-
|
|
92
|
-
const original = {
|
|
93
|
-
position: [...movingNode.position] as [number, number, number],
|
|
94
|
-
rotation: movingNode.rotation,
|
|
95
|
-
parentId: movingNode.parentId,
|
|
96
|
-
metadata: movingNode.metadata,
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Track whether the move was committed so cleanup knows whether to revert.
|
|
100
|
-
// We avoid setting isTransient on the store to prevent RoofSystem from
|
|
101
|
-
// resetting the mesh position (it resets on dirty) and from triggering
|
|
102
|
-
// expensive merged-mesh CSG rebuilds on every frame.
|
|
103
|
-
let wasCommitted = false
|
|
104
|
-
let wasCancelled = false
|
|
105
|
-
|
|
106
|
-
// Track pending rotation — no store updates during drag
|
|
107
|
-
let pendingRotation: number = movingNode.rotation as number
|
|
108
|
-
|
|
109
|
-
// For roof-segment moves: the selection was cleared before entering move mode,
|
|
110
|
-
// so isSelected=false on the parent roof, hiding individual segment meshes and
|
|
111
|
-
// showing only the merged mesh. We directly flip Three.js visibility so the
|
|
112
|
-
// user sees the individual segment tracking the cursor.
|
|
113
|
-
let segmentWrapperGroup: THREE.Object3D | null = null
|
|
114
|
-
let mergedRoofMesh: THREE.Object3D | null = null
|
|
115
|
-
if (movingNode.type === 'roof-segment' || movingNode.type === 'stair-segment') {
|
|
116
|
-
const segmentMesh = sceneRegistry.nodes.get(movingNode.id)
|
|
117
|
-
if (segmentMesh?.parent) {
|
|
118
|
-
// segmentMesh.parent = <group visible={isSelected}> wrapper in Roof/StairRenderer
|
|
119
|
-
// segmentMesh.parent.parent = the registered roof/stair group
|
|
120
|
-
segmentWrapperGroup = segmentMesh.parent
|
|
121
|
-
const mergedName = movingNode.type === 'stair-segment' ? 'merged-stair' : 'merged-roof'
|
|
122
|
-
mergedRoofMesh = segmentMesh.parent.parent?.getObjectByName(mergedName) ?? null
|
|
123
|
-
segmentWrapperGroup.visible = true
|
|
124
|
-
if (mergedRoofMesh) mergedRoofMesh.visible = false
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const resolveLevelId = () => {
|
|
129
|
-
if (movingNode.type === 'roof' || movingNode.type === 'stair') {
|
|
130
|
-
return movingNode.parentId ?? null
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
(movingNode.type === 'roof-segment' || movingNode.type === 'stair-segment') &&
|
|
135
|
-
movingNode.parentId
|
|
136
|
-
) {
|
|
137
|
-
const parentNode = useScene.getState().nodes[movingNode.parentId as AnyNodeId]
|
|
138
|
-
return parentNode && 'parentId' in parentNode ? (parentNode.parentId ?? null) : null
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return null
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const levelId = resolveLevelId()
|
|
145
|
-
const levelNode =
|
|
146
|
-
levelId && useScene.getState().nodes[levelId as AnyNodeId]?.type === 'level'
|
|
147
|
-
? (useScene.getState().nodes[levelId as AnyNodeId] as LevelNode)
|
|
148
|
-
: null
|
|
149
|
-
const levelChildren = levelNode?.children ?? []
|
|
150
|
-
const levelWalls = levelChildren
|
|
151
|
-
.map((childId) => useScene.getState().nodes[childId as AnyNodeId])
|
|
152
|
-
.filter((node): node is WallNode => node?.type === 'wall')
|
|
153
|
-
const levelFences = levelChildren
|
|
154
|
-
.map((childId) => useScene.getState().nodes[childId as AnyNodeId])
|
|
155
|
-
.filter((node): node is FenceNode => node?.type === 'fence')
|
|
156
|
-
const buildingId = useViewer.getState().selection.buildingId
|
|
157
|
-
const buildingObj = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
|
|
158
|
-
|
|
159
|
-
const localToWorldPoint = (localPoint: WallPlanPoint, y: number): [number, number, number] => {
|
|
160
|
-
if (buildingObj) {
|
|
161
|
-
const worldPoint = buildingObj.localToWorld(new THREE.Vector3(localPoint[0], y, localPoint[1]))
|
|
162
|
-
return [worldPoint.x, worldPoint.y, worldPoint.z]
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return [localPoint[0], y, localPoint[1]]
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const computeLocal = (
|
|
169
|
-
gridX: number,
|
|
170
|
-
gridZ: number,
|
|
171
|
-
y: number,
|
|
172
|
-
buildingLocalX: number,
|
|
173
|
-
buildingLocalZ: number,
|
|
174
|
-
): [number, number] => {
|
|
175
|
-
// Segments have a transformed parent (stair/roof). Convert world → parent-local
|
|
176
|
-
// via Three.js hierarchy so the segment's stored position stays parent-relative.
|
|
177
|
-
if (
|
|
178
|
-
(movingNode.type === 'roof-segment' || movingNode.type === 'stair-segment') &&
|
|
179
|
-
movingNode.parentId
|
|
180
|
-
) {
|
|
181
|
-
const parentNode = useScene.getState().nodes[movingNode.parentId as AnyNodeId]
|
|
182
|
-
if (parentNode && 'position' in parentNode && 'rotation' in parentNode) {
|
|
183
|
-
const parentObj = sceneRegistry.nodes.get(movingNode.parentId)
|
|
184
|
-
if (parentObj) {
|
|
185
|
-
const worldVec = new THREE.Vector3(gridX, y, gridZ)
|
|
186
|
-
parentObj.worldToLocal(worldVec)
|
|
187
|
-
return [worldVec.x, worldVec.z]
|
|
188
|
-
}
|
|
189
|
-
const dx = gridX - (parentNode.position[0] as number)
|
|
190
|
-
const dz = gridZ - (parentNode.position[2] as number)
|
|
191
|
-
const angle = -(parentNode.rotation as number)
|
|
192
|
-
return [
|
|
193
|
-
dx * Math.cos(angle) - dz * Math.sin(angle),
|
|
194
|
-
dx * Math.sin(angle) + dz * Math.cos(angle),
|
|
195
|
-
]
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Stair/roof live directly in the level — their stored position is building-local.
|
|
200
|
-
// event.localPosition is already building-local, so using it handles building rotation.
|
|
201
|
-
return [buildingLocalX, buildingLocalZ]
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const onGridMove = (event: GridEvent) => {
|
|
205
|
-
const y = event.position[1]
|
|
206
|
-
|
|
207
|
-
const snappedLocal = snapFenceDraftPoint({
|
|
208
|
-
point: [event.localPosition[0], event.localPosition[2]],
|
|
209
|
-
walls: levelWalls,
|
|
210
|
-
fences: levelFences,
|
|
211
|
-
})
|
|
212
|
-
const [gridX, , gridZ] = localToWorldPoint(snappedLocal, y)
|
|
213
|
-
|
|
214
|
-
if (previousGridPosRef.current && (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])) {
|
|
215
|
-
sfxEmitter.emit('sfx:grid-snap')
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
previousGridPosRef.current = [gridX, gridZ]
|
|
219
|
-
const [lx, lz] = snappedLocal
|
|
220
|
-
setCursorWorldPos([lx, event.localPosition[1], lz])
|
|
221
|
-
|
|
222
|
-
const [localX, localZ] = computeLocal(gridX, gridZ, y, lx, lz)
|
|
223
|
-
|
|
224
|
-
// Directly update the Three.js mesh — no store update during drag
|
|
225
|
-
const mesh = sceneRegistry.nodes.get(movingNode.id)
|
|
226
|
-
if (mesh) {
|
|
227
|
-
mesh.position.x = localX
|
|
228
|
-
mesh.position.z = localZ
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Publish world-space position so the 2D floorplan can track the drag
|
|
232
|
-
useLiveTransforms.getState().set(movingNode.id, {
|
|
233
|
-
position: [gridX, y, gridZ],
|
|
234
|
-
rotation: pendingRotation,
|
|
235
|
-
})
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const onGridClick = (event: GridEvent) => {
|
|
239
|
-
const y = event.position[1]
|
|
240
|
-
const snappedLocal = snapFenceDraftPoint({
|
|
241
|
-
point: [event.localPosition[0], event.localPosition[2]],
|
|
242
|
-
walls: levelWalls,
|
|
243
|
-
fences: levelFences,
|
|
244
|
-
})
|
|
245
|
-
const [gridX, , gridZ] = localToWorldPoint(snappedLocal, y)
|
|
246
|
-
const [lx, lz] = snappedLocal
|
|
247
|
-
|
|
248
|
-
const [localX, localZ] = computeLocal(gridX, gridZ, y, lx, lz)
|
|
249
|
-
|
|
250
|
-
wasCommitted = true
|
|
251
|
-
|
|
252
|
-
// The store still holds the original values (we didn't update during drag).
|
|
253
|
-
// Resume temporal and apply the final state as a single undoable step.
|
|
254
|
-
useScene.temporal.getState().resume()
|
|
255
|
-
|
|
256
|
-
if (isNew && movingNode.type === 'roof') {
|
|
257
|
-
clearRoofDuplicateMetadata(movingNode.id as AnyNodeId, {
|
|
258
|
-
position: [localX, movingNode.position[1], localZ],
|
|
259
|
-
rotation: pendingRotation,
|
|
260
|
-
metadata: committedMeta,
|
|
261
|
-
})
|
|
262
|
-
} else {
|
|
263
|
-
useScene.getState().updateNode(movingNode.id, {
|
|
264
|
-
position: [localX, movingNode.position[1], localZ],
|
|
265
|
-
rotation: pendingRotation,
|
|
266
|
-
metadata: committedMeta,
|
|
267
|
-
})
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
useScene.temporal.getState().pause()
|
|
271
|
-
|
|
272
|
-
sfxEmitter.emit('sfx:item-place')
|
|
273
|
-
useViewer.getState().setSelection({ selectedIds: [movingNode.id] })
|
|
274
|
-
useLiveTransforms.getState().clear(movingNode.id)
|
|
275
|
-
exitMoveMode()
|
|
276
|
-
event.nativeEvent?.stopPropagation?.()
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const onCancel = () => {
|
|
280
|
-
wasCancelled = true
|
|
281
|
-
useLiveTransforms.getState().clear(movingNode.id)
|
|
282
|
-
if (isNew) {
|
|
283
|
-
useScene.getState().deleteNode(movingNode.id)
|
|
284
|
-
} else {
|
|
285
|
-
useScene.getState().updateNode(movingNode.id, {
|
|
286
|
-
position: original.position,
|
|
287
|
-
rotation: original.rotation,
|
|
288
|
-
metadata: original.metadata,
|
|
289
|
-
})
|
|
290
|
-
}
|
|
291
|
-
useScene.temporal.getState().resume()
|
|
292
|
-
exitMoveMode()
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
296
|
-
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
|
297
|
-
return
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const ROTATION_STEP = Math.PI / 4
|
|
301
|
-
let rotationDelta = 0
|
|
302
|
-
if (event.key === 'r' || event.key === 'R') rotationDelta = ROTATION_STEP
|
|
303
|
-
else if (event.key === 't' || event.key === 'T') rotationDelta = -ROTATION_STEP
|
|
304
|
-
|
|
305
|
-
if (rotationDelta !== 0) {
|
|
306
|
-
event.preventDefault()
|
|
307
|
-
sfxEmitter.emit('sfx:item-rotate')
|
|
308
|
-
|
|
309
|
-
pendingRotation += rotationDelta
|
|
310
|
-
|
|
311
|
-
// Directly update the Three.js mesh — no store update during drag
|
|
312
|
-
const mesh = sceneRegistry.nodes.get(movingNode.id)
|
|
313
|
-
if (mesh) mesh.rotation.y = pendingRotation
|
|
314
|
-
|
|
315
|
-
// Update live transform rotation for 2D floorplan
|
|
316
|
-
const currentLive = useLiveTransforms.getState().get(movingNode.id)
|
|
317
|
-
if (currentLive) {
|
|
318
|
-
useLiveTransforms.getState().set(movingNode.id, {
|
|
319
|
-
...currentLive,
|
|
320
|
-
rotation: pendingRotation,
|
|
321
|
-
})
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
emitter.on('grid:move', onGridMove)
|
|
327
|
-
emitter.on('grid:click', onGridClick)
|
|
328
|
-
emitter.on('tool:cancel', onCancel)
|
|
329
|
-
window.addEventListener('keydown', onKeyDown)
|
|
330
|
-
|
|
331
|
-
return () => {
|
|
332
|
-
// Restore segment wrapper visibility (React will re-sync on next render)
|
|
333
|
-
if (segmentWrapperGroup) segmentWrapperGroup.visible = false
|
|
334
|
-
if (mergedRoofMesh) mergedRoofMesh.visible = true
|
|
335
|
-
|
|
336
|
-
// Clear ephemeral live transform
|
|
337
|
-
useLiveTransforms.getState().clear(movingNode.id)
|
|
338
|
-
|
|
339
|
-
if (!(wasCommitted || wasCancelled || isNew)) {
|
|
340
|
-
useScene.getState().updateNode(movingNode.id, {
|
|
341
|
-
position: original.position,
|
|
342
|
-
rotation: original.rotation,
|
|
343
|
-
metadata: original.metadata,
|
|
344
|
-
})
|
|
345
|
-
}
|
|
346
|
-
useScene.temporal.getState().resume()
|
|
347
|
-
emitter.off('grid:move', onGridMove)
|
|
348
|
-
emitter.off('grid:click', onGridClick)
|
|
349
|
-
emitter.off('tool:cancel', onCancel)
|
|
350
|
-
window.removeEventListener('keydown', onKeyDown)
|
|
351
|
-
}
|
|
352
|
-
}, [movingNode, exitMoveMode])
|
|
353
|
-
|
|
354
|
-
return (
|
|
355
|
-
<group>
|
|
356
|
-
<CursorSphere position={cursorWorldPos} showTooltip={false} />
|
|
357
|
-
</group>
|
|
358
|
-
)
|
|
359
|
-
}
|