@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
|
@@ -9,24 +9,28 @@ interface MetricControlProps {
|
|
|
9
9
|
label: React.ReactNode
|
|
10
10
|
value: number
|
|
11
11
|
onChange: (value: number) => void
|
|
12
|
+
onCommit?: (value: number) => void
|
|
12
13
|
min?: number
|
|
13
14
|
max?: number
|
|
14
15
|
precision?: number
|
|
15
16
|
step?: number
|
|
16
17
|
className?: string
|
|
17
18
|
unit?: string
|
|
19
|
+
restoreOnCommit?: boolean
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export function MetricControl({
|
|
21
23
|
label,
|
|
22
24
|
value,
|
|
23
25
|
onChange,
|
|
26
|
+
onCommit,
|
|
24
27
|
min = Number.NEGATIVE_INFINITY,
|
|
25
28
|
max = Number.POSITIVE_INFINITY,
|
|
26
29
|
precision = 2,
|
|
27
30
|
step = 1,
|
|
28
31
|
className,
|
|
29
32
|
unit = '',
|
|
33
|
+
restoreOnCommit = true,
|
|
30
34
|
}: MetricControlProps) {
|
|
31
35
|
const viewerUnit = useViewer((state) => state.unit)
|
|
32
36
|
const isImperial = viewerUnit === 'imperial' && unit === 'm'
|
|
@@ -53,6 +57,17 @@ export function MetricControl({
|
|
|
53
57
|
[min, max],
|
|
54
58
|
)
|
|
55
59
|
|
|
60
|
+
const applyCommittedValue = useCallback(
|
|
61
|
+
(nextValue: number) => {
|
|
62
|
+
if (onCommit) {
|
|
63
|
+
onCommit(nextValue)
|
|
64
|
+
} else {
|
|
65
|
+
onChange(nextValue)
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
[onChange, onCommit],
|
|
69
|
+
)
|
|
70
|
+
|
|
56
71
|
useEffect(() => {
|
|
57
72
|
if (!isEditing) {
|
|
58
73
|
setInputValue(displayValue.toFixed(precision))
|
|
@@ -77,13 +92,13 @@ export function MetricControl({
|
|
|
77
92
|
const finalValue = Number.parseFloat((newValue * multiplier).toFixed(precision)) / multiplier
|
|
78
93
|
|
|
79
94
|
if (Math.abs(finalValue - valueRef.current) > 1e-6) {
|
|
80
|
-
|
|
95
|
+
applyCommittedValue(finalValue)
|
|
81
96
|
}
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
container.addEventListener('wheel', handleWheel, { passive: false })
|
|
85
100
|
return () => container.removeEventListener('wheel', handleWheel)
|
|
86
|
-
}, [isEditing, step, clamp,
|
|
101
|
+
}, [isEditing, step, clamp, applyCommittedValue, precision, multiplier])
|
|
87
102
|
|
|
88
103
|
useEffect(() => {
|
|
89
104
|
if (!isHovered || isEditing) return
|
|
@@ -104,14 +119,14 @@ export function MetricControl({
|
|
|
104
119
|
Number.parseFloat((newValue * multiplier).toFixed(precision)) / multiplier
|
|
105
120
|
|
|
106
121
|
if (Math.abs(finalValue - valueRef.current) > 1e-6) {
|
|
107
|
-
|
|
122
|
+
applyCommittedValue(finalValue)
|
|
108
123
|
}
|
|
109
124
|
}
|
|
110
125
|
}
|
|
111
126
|
|
|
112
127
|
window.addEventListener('keydown', handleKeyDown)
|
|
113
128
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
114
|
-
}, [isHovered, isEditing, step, clamp,
|
|
129
|
+
}, [isHovered, isEditing, step, clamp, applyCommittedValue, precision, multiplier])
|
|
115
130
|
|
|
116
131
|
const handlePointerDown = useCallback(
|
|
117
132
|
(e: React.PointerEvent) => {
|
|
@@ -148,7 +163,14 @@ export function MetricControl({
|
|
|
148
163
|
document.removeEventListener('pointermove', handlePointerMove)
|
|
149
164
|
document.removeEventListener('pointerup', handlePointerUp)
|
|
150
165
|
|
|
151
|
-
|
|
166
|
+
const changed = Math.abs(finalValue - startValueRef.current) > 1e-6
|
|
167
|
+
if (onCommit) {
|
|
168
|
+
if (changed && restoreOnCommit) {
|
|
169
|
+
onChange(startValueRef.current)
|
|
170
|
+
}
|
|
171
|
+
useScene.temporal.getState().resume()
|
|
172
|
+
onCommit(finalValue)
|
|
173
|
+
} else if (changed) {
|
|
152
174
|
onChange(startValueRef.current)
|
|
153
175
|
useScene.temporal.getState().resume()
|
|
154
176
|
onChange(finalValue)
|
|
@@ -160,7 +182,7 @@ export function MetricControl({
|
|
|
160
182
|
document.addEventListener('pointermove', handlePointerMove)
|
|
161
183
|
document.addEventListener('pointerup', handlePointerUp)
|
|
162
184
|
},
|
|
163
|
-
[isEditing, value, onChange, clamp, precision, step, multiplier],
|
|
185
|
+
[isEditing, value, onChange, onCommit, restoreOnCommit, clamp, precision, step, multiplier],
|
|
164
186
|
)
|
|
165
187
|
|
|
166
188
|
const handleValueClick = useCallback(() => {
|
|
@@ -177,10 +199,10 @@ export function MetricControl({
|
|
|
177
199
|
if (Number.isNaN(numValue)) {
|
|
178
200
|
setInputValue((value * multiplier).toFixed(precision))
|
|
179
201
|
} else {
|
|
180
|
-
|
|
202
|
+
applyCommittedValue(clamp(numValue / multiplier))
|
|
181
203
|
}
|
|
182
204
|
setIsEditing(false)
|
|
183
|
-
}, [inputValue,
|
|
205
|
+
}, [inputValue, applyCommittedValue, clamp, multiplier, value, precision])
|
|
184
206
|
|
|
185
207
|
const handleInputBlur = useCallback(() => {
|
|
186
208
|
submitValue()
|
|
@@ -196,16 +218,16 @@ export function MetricControl({
|
|
|
196
218
|
} else if (e.key === 'ArrowUp') {
|
|
197
219
|
e.preventDefault()
|
|
198
220
|
const newV = clamp(value + step / multiplier)
|
|
199
|
-
|
|
221
|
+
applyCommittedValue(newV)
|
|
200
222
|
setInputValue((newV * multiplier).toFixed(precision))
|
|
201
223
|
} else if (e.key === 'ArrowDown') {
|
|
202
224
|
e.preventDefault()
|
|
203
225
|
const newV = clamp(value - step / multiplier)
|
|
204
|
-
|
|
226
|
+
applyCommittedValue(newV)
|
|
205
227
|
setInputValue((newV * multiplier).toFixed(precision))
|
|
206
228
|
}
|
|
207
229
|
},
|
|
208
|
-
[submitValue, value, multiplier, precision, step, clamp,
|
|
230
|
+
[submitValue, value, multiplier, precision, step, clamp, applyCommittedValue],
|
|
209
231
|
)
|
|
210
232
|
|
|
211
233
|
return (
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
useScene,
|
|
27
27
|
} from '@pascal-app/core'
|
|
28
28
|
import { useViewer } from '@pascal-app/viewer'
|
|
29
|
-
import { Copy, GripVertical, MoreVertical, Plus, Trash2 } from 'lucide-react'
|
|
29
|
+
import { ClipboardPaste, Copy, GripVertical, MoreVertical, Plus, Trash2 } from 'lucide-react'
|
|
30
30
|
import {
|
|
31
31
|
type ButtonHTMLAttributes,
|
|
32
32
|
type CSSProperties,
|
|
@@ -34,13 +34,21 @@ import {
|
|
|
34
34
|
useEffect,
|
|
35
35
|
useRef,
|
|
36
36
|
useState,
|
|
37
|
+
useSyncExternalStore,
|
|
37
38
|
} from 'react'
|
|
38
39
|
import { useShallow } from 'zustand/react/shallow'
|
|
39
40
|
import {
|
|
40
41
|
buildLevelDuplicateCreateOps,
|
|
41
42
|
type LevelDuplicatePreset,
|
|
42
43
|
} from '../../lib/level-duplication'
|
|
44
|
+
import { getDefaultLevelName, getLevelDisplayName } from '../../lib/level-name'
|
|
43
45
|
import { deleteLevelWithFallbackSelection } from '../../lib/level-selection'
|
|
46
|
+
import {
|
|
47
|
+
getEditorClipboardSnapshot,
|
|
48
|
+
pasteEditorClipboardToLevel,
|
|
49
|
+
subscribeEditorClipboard,
|
|
50
|
+
} from '../../lib/scene-clipboard'
|
|
51
|
+
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
44
52
|
import { cn } from '../../lib/utils'
|
|
45
53
|
import { LevelDuplicateDialog } from './level-duplicate-dialog'
|
|
46
54
|
import {
|
|
@@ -53,10 +61,6 @@ import {
|
|
|
53
61
|
} from './primitives/dialog'
|
|
54
62
|
import { Popover, PopoverContent, PopoverTrigger } from './primitives/popover'
|
|
55
63
|
|
|
56
|
-
function getLevelDisplayLabel(level: LevelNode) {
|
|
57
|
-
return level.name || `Level ${level.level}`
|
|
58
|
-
}
|
|
59
|
-
|
|
60
64
|
// ── Inline rename input for a level row ─────────────────────────────────────
|
|
61
65
|
|
|
62
66
|
function LevelInlineRename({
|
|
@@ -69,7 +73,7 @@ function LevelInlineRename({
|
|
|
69
73
|
onStopEditing: () => void
|
|
70
74
|
}) {
|
|
71
75
|
const updateNode = useScene((s) => s.updateNode)
|
|
72
|
-
const defaultName =
|
|
76
|
+
const defaultName = getDefaultLevelName(level.level)
|
|
73
77
|
const [value, setValue] = useState(level.name || '')
|
|
74
78
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
75
79
|
|
|
@@ -126,6 +130,7 @@ function LevelRow({
|
|
|
126
130
|
dragHandleRef,
|
|
127
131
|
onSelect,
|
|
128
132
|
onDuplicate,
|
|
133
|
+
onPaste,
|
|
129
134
|
onRequestDelete,
|
|
130
135
|
}: {
|
|
131
136
|
level: LevelNode
|
|
@@ -135,6 +140,7 @@ function LevelRow({
|
|
|
135
140
|
dragHandleRef?: (element: HTMLButtonElement | null) => void
|
|
136
141
|
onSelect: () => void
|
|
137
142
|
onDuplicate: (preset?: LevelDuplicatePreset) => void
|
|
143
|
+
onPaste?: () => void
|
|
138
144
|
onRequestDelete: () => void
|
|
139
145
|
}) {
|
|
140
146
|
const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false)
|
|
@@ -160,7 +166,7 @@ function LevelRow({
|
|
|
160
166
|
>
|
|
161
167
|
<button
|
|
162
168
|
{...dragHandleProps}
|
|
163
|
-
aria-label={`Reorder ${
|
|
169
|
+
aria-label={`Reorder ${getLevelDisplayName(level)}`}
|
|
164
170
|
className={cn(
|
|
165
171
|
'ml-0.5 flex h-6 w-4 shrink-0 cursor-grab touch-none items-center justify-center rounded-md text-muted-foreground/35 opacity-0 transition-colors hover:bg-white/5 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 group-hover/level:opacity-100',
|
|
166
172
|
isDragging && 'cursor-grabbing opacity-100',
|
|
@@ -183,10 +189,10 @@ function LevelRow({
|
|
|
183
189
|
e.stopPropagation()
|
|
184
190
|
setIsEditing(true)
|
|
185
191
|
}}
|
|
186
|
-
title={
|
|
192
|
+
title={getLevelDisplayName(level)}
|
|
187
193
|
type="button"
|
|
188
194
|
>
|
|
189
|
-
<span className="truncate">{
|
|
195
|
+
<span className="truncate">{getLevelDisplayName(level)}</span>
|
|
190
196
|
</button>
|
|
191
197
|
|
|
192
198
|
{/* Vertical three-dot menu — inside the pill */}
|
|
@@ -223,6 +229,19 @@ function LevelRow({
|
|
|
223
229
|
<Copy className="h-3 w-3" />
|
|
224
230
|
Duplicate with options...
|
|
225
231
|
</button>
|
|
232
|
+
{onPaste && (
|
|
233
|
+
<button
|
|
234
|
+
className="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-muted-foreground text-xs transition-colors hover:bg-white/10 hover:text-foreground"
|
|
235
|
+
onClick={(e) => {
|
|
236
|
+
e.stopPropagation()
|
|
237
|
+
onPaste()
|
|
238
|
+
}}
|
|
239
|
+
type="button"
|
|
240
|
+
>
|
|
241
|
+
<ClipboardPaste className="h-3 w-3" />
|
|
242
|
+
Paste copied selection
|
|
243
|
+
</button>
|
|
244
|
+
)}
|
|
226
245
|
<button
|
|
227
246
|
className="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-muted-foreground text-xs transition-colors hover:bg-white/10 hover:text-red-400"
|
|
228
247
|
onClick={(e) => {
|
|
@@ -256,12 +275,14 @@ function SortableLevelRow({
|
|
|
256
275
|
isSelected,
|
|
257
276
|
onSelect,
|
|
258
277
|
onDuplicate,
|
|
278
|
+
onPaste,
|
|
259
279
|
onRequestDelete,
|
|
260
280
|
}: {
|
|
261
281
|
level: LevelNode
|
|
262
282
|
isSelected: boolean
|
|
263
283
|
onSelect: () => void
|
|
264
284
|
onDuplicate: (preset?: LevelDuplicatePreset) => void
|
|
285
|
+
onPaste?: () => void
|
|
265
286
|
onRequestDelete: () => void
|
|
266
287
|
}) {
|
|
267
288
|
const {
|
|
@@ -291,6 +312,7 @@ function SortableLevelRow({
|
|
|
291
312
|
isSelected={isSelected}
|
|
292
313
|
level={level}
|
|
293
314
|
onDuplicate={onDuplicate}
|
|
315
|
+
onPaste={onPaste}
|
|
294
316
|
onRequestDelete={onRequestDelete}
|
|
295
317
|
onSelect={onSelect}
|
|
296
318
|
/>
|
|
@@ -310,6 +332,11 @@ export function FloatingLevelSelector() {
|
|
|
310
332
|
|
|
311
333
|
const [deletingLevel, setDeletingLevel] = useState<LevelNode | null>(null)
|
|
312
334
|
const [draggingLevelId, setDraggingLevelId] = useState<string | null>(null)
|
|
335
|
+
const clipboardSnapshot = useSyncExternalStore(
|
|
336
|
+
subscribeEditorClipboard,
|
|
337
|
+
getEditorClipboardSnapshot,
|
|
338
|
+
getEditorClipboardSnapshot,
|
|
339
|
+
)
|
|
313
340
|
const sensors = useSensors(
|
|
314
341
|
useSensor(PointerSensor, {
|
|
315
342
|
activationConstraint: { distance: 4 },
|
|
@@ -424,6 +451,13 @@ export function FloatingLevelSelector() {
|
|
|
424
451
|
[createNodes, levels, resolvedBuildingId, setSelection, updateNodes],
|
|
425
452
|
)
|
|
426
453
|
|
|
454
|
+
const handlePasteToLevel = useCallback((level: LevelNode) => {
|
|
455
|
+
const result = pasteEditorClipboardToLevel(level.id)
|
|
456
|
+
if (result?.pastedIds.length) {
|
|
457
|
+
sfxEmitter.emit('sfx:item-place')
|
|
458
|
+
}
|
|
459
|
+
}, [])
|
|
460
|
+
|
|
427
461
|
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
428
462
|
setDraggingLevelId(String(event.active.id))
|
|
429
463
|
}, [])
|
|
@@ -523,6 +557,9 @@ export function FloatingLevelSelector() {
|
|
|
523
557
|
isSelected={isSelected}
|
|
524
558
|
level={level}
|
|
525
559
|
onDuplicate={(preset) => handleDuplicateLevel(level, preset)}
|
|
560
|
+
onPaste={
|
|
561
|
+
clipboardSnapshot ? () => handlePasteToLevel(level) : undefined
|
|
562
|
+
}
|
|
526
563
|
onRequestDelete={() => setDeletingLevel(level)}
|
|
527
564
|
onSelect={() =>
|
|
528
565
|
setSelection(
|
|
@@ -559,7 +596,7 @@ export function FloatingLevelSelector() {
|
|
|
559
596
|
<DialogTitle>Delete level</DialogTitle>
|
|
560
597
|
<DialogDescription>
|
|
561
598
|
Are you sure you want to delete{' '}
|
|
562
|
-
<strong>{deletingLevel ?
|
|
599
|
+
<strong>{deletingLevel ? getLevelDisplayName(deletingLevel) : ''}</strong>? All
|
|
563
600
|
walls, floors, and objects on this level will be permanently removed.
|
|
564
601
|
</DialogDescription>
|
|
565
602
|
</DialogHeader>
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { nodeRegistry } from '@pascal-app/core'
|
|
3
4
|
import { useIsMobile } from '../../../hooks/use-mobile'
|
|
4
5
|
import useEditor from '../../../store/use-editor'
|
|
5
6
|
import { BuildingHelper } from './building-helper'
|
|
6
|
-
import { CeilingHelper } from './ceiling-helper'
|
|
7
7
|
import { ItemHelper } from './item-helper'
|
|
8
|
+
import { RegisteredToolHelper } from './registered-tool-helper'
|
|
8
9
|
import { RoofHelper } from './roof-helper'
|
|
9
|
-
import { SlabHelper } from './slab-helper'
|
|
10
|
-
import { WallHelper } from './wall-helper'
|
|
11
10
|
|
|
12
11
|
export function HelperManager() {
|
|
13
12
|
const mode = useEditor((s) => s.mode)
|
|
@@ -27,19 +26,19 @@ export function HelperManager() {
|
|
|
27
26
|
return null
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
case 'ceiling':
|
|
39
|
-
return <CeilingHelper />
|
|
40
|
-
case 'roof':
|
|
41
|
-
return <RoofHelper />
|
|
42
|
-
default:
|
|
43
|
-
return null
|
|
29
|
+
// Registry-first: kinds with `def.toolHints` render through the generic
|
|
30
|
+
// `RegisteredToolHelper`. Today that covers ceiling / door / fence /
|
|
31
|
+
// item / shelf / slab / spawn / wall / window.
|
|
32
|
+
if (tool) {
|
|
33
|
+
const def = nodeRegistry.get(tool)
|
|
34
|
+
if (def?.toolHints && def.toolHints.length > 0) {
|
|
35
|
+
return <RegisteredToolHelper hints={def.toolHints} />
|
|
36
|
+
}
|
|
44
37
|
}
|
|
38
|
+
|
|
39
|
+
// Legacy fallback — only `roof` remains because it hasn't migrated to
|
|
40
|
+
// `def.tool` / `def.toolHints` yet (no Stage D port). When roof
|
|
41
|
+
// migrates, this switch deletes outright.
|
|
42
|
+
if (tool === 'roof') return <RoofHelper />
|
|
43
|
+
return null
|
|
45
44
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ToolHint } from '@pascal-app/core'
|
|
2
|
+
import { ShortcutToken } from '../primitives/shortcut-token'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generic helper panel rendered from `def.toolHints` data. Matches the
|
|
6
|
+
* visual styling of the hand-written `<WallHelper>` / `<ItemHelper>` /
|
|
7
|
+
* etc. so registry-driven kinds get a consistent look without each kind
|
|
8
|
+
* writing its own component.
|
|
9
|
+
*
|
|
10
|
+
* Drops the need for per-kind helper files entirely — kinds declare
|
|
11
|
+
* their hints as static data in their `NodeDefinition`.
|
|
12
|
+
*/
|
|
13
|
+
export function RegisteredToolHelper({ hints }: { hints: ToolHint[] }) {
|
|
14
|
+
if (hints.length === 0) return null
|
|
15
|
+
return (
|
|
16
|
+
<div className="pointer-events-none fixed top-1/2 right-4 z-40 flex -translate-y-1/2 flex-col gap-2 rounded-lg border border-border bg-background/95 px-4 py-3 shadow-lg backdrop-blur-md">
|
|
17
|
+
{hints.map((hint) => (
|
|
18
|
+
<div className="flex items-center gap-2 text-sm" key={`${hint.key}:${hint.label}`}>
|
|
19
|
+
<ShortcutToken value={hint.key} />
|
|
20
|
+
<span className="text-muted-foreground">{hint.label}</span>
|
|
21
|
+
</div>
|
|
22
|
+
))}
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
TooltipContent,
|
|
10
10
|
TooltipTrigger,
|
|
11
11
|
} from './../../../components/ui/primitives/tooltip'
|
|
12
|
+
import { triggerSFX } from './../../../lib/sfx-bus'
|
|
12
13
|
import { cn } from './../../../lib/utils'
|
|
13
14
|
import useEditor, { type CatalogCategory } from './../../../store/use-editor'
|
|
14
15
|
import { CATALOG_ITEMS } from './catalog-items'
|
|
@@ -94,10 +95,12 @@ export function ItemCatalog({
|
|
|
94
95
|
)}
|
|
95
96
|
key={index}
|
|
96
97
|
onClick={() => {
|
|
98
|
+
triggerSFX('sfx:menu-click')
|
|
97
99
|
setSelectedItem(item)
|
|
98
100
|
setTool('item')
|
|
99
101
|
setMode('build')
|
|
100
102
|
}}
|
|
103
|
+
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
|
101
104
|
type="button"
|
|
102
105
|
>
|
|
103
106
|
<div className="relative aspect-square w-full overflow-hidden rounded-lg">
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { LevelNode } from '@pascal-app/core'
|
|
4
4
|
import { useEffect, useState } from 'react'
|
|
5
5
|
import type { LevelDuplicatePreset } from '../../lib/level-duplication'
|
|
6
|
+
import { getLevelDisplayName } from '../../lib/level-name'
|
|
6
7
|
import { cn } from '../../lib/utils'
|
|
7
8
|
import {
|
|
8
9
|
Dialog,
|
|
@@ -42,7 +43,7 @@ const DUPLICATE_PRESETS: Array<{
|
|
|
42
43
|
|
|
43
44
|
function getLevelLabel(level: LevelNode | null) {
|
|
44
45
|
if (!level) return 'this level'
|
|
45
|
-
return level
|
|
46
|
+
return getLevelDisplayName(level)
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export function LevelDuplicateDialog({
|
|
@@ -13,6 +13,7 @@ const TYPE_DEFAULTS: Record<string, NodeDisplay> = {
|
|
|
13
13
|
slab: { icon: '/icons/floor.png', label: 'Slab' },
|
|
14
14
|
ceiling: { icon: '/icons/ceiling.png', label: 'Ceiling' },
|
|
15
15
|
column: { icon: '/icons/column.png', label: 'Column' },
|
|
16
|
+
elevator: { icon: '/icons/elevator.png', label: 'Elevator' },
|
|
16
17
|
fence: { icon: '/icons/fence.png', label: 'Fence' },
|
|
17
18
|
roof: { icon: '/icons/roof.png', label: 'Roof' },
|
|
18
19
|
'roof-segment': { icon: '/icons/roof.png', label: 'Roof segment' },
|
|
@@ -5,8 +5,11 @@ import {
|
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
type BuildingNode,
|
|
7
7
|
type CeilingNode,
|
|
8
|
+
type ChimneyNode,
|
|
8
9
|
type ColumnNode,
|
|
9
10
|
type DoorNode,
|
|
11
|
+
type DormerNode,
|
|
12
|
+
type ElevatorNode,
|
|
10
13
|
type FenceNode,
|
|
11
14
|
type ItemNode,
|
|
12
15
|
type RoofNode,
|
|
@@ -23,31 +26,22 @@ import { useCallback, useEffect, useState } from 'react'
|
|
|
23
26
|
import { useIsMobile } from '../../../hooks/use-mobile'
|
|
24
27
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
25
28
|
import useEditor from '../../../store/use-editor'
|
|
26
|
-
import { CeilingPanel } from './ceiling-panel'
|
|
27
|
-
import { ColumnPanel } from './column-panel'
|
|
28
|
-
import { DoorPanel } from './door-panel'
|
|
29
|
-
import { FencePanel } from './fence-panel'
|
|
30
|
-
import { ItemPanel } from './item-panel'
|
|
31
29
|
import { MobilePanelSheet } from './mobile-panel-sheet'
|
|
32
30
|
import { MobileSelectionBar } from './mobile-selection-bar'
|
|
33
31
|
import { getNodeDisplay } from './node-display'
|
|
34
32
|
import { PaintPanel } from './paint-panel'
|
|
33
|
+
import { ParametricInspector } from './parametric-inspector'
|
|
35
34
|
import { ReferencePanel } from './reference-panel'
|
|
36
|
-
import { RoofPanel } from './roof-panel'
|
|
37
|
-
import { RoofSegmentPanel } from './roof-segment-panel'
|
|
38
|
-
import { SlabPanel } from './slab-panel'
|
|
39
|
-
import { SpawnPanel } from './spawn-panel'
|
|
40
|
-
import { StairPanel } from './stair-panel'
|
|
41
|
-
import { StairSegmentPanel } from './stair-segment-panel'
|
|
42
|
-
import { WallPanel } from './wall-panel'
|
|
43
|
-
import { WindowPanel } from './window-panel'
|
|
44
35
|
|
|
45
36
|
type MovableNode =
|
|
46
37
|
| ItemNode
|
|
47
38
|
| WindowNode
|
|
48
39
|
| DoorNode
|
|
40
|
+
| ElevatorNode
|
|
49
41
|
| CeilingNode
|
|
42
|
+
| ChimneyNode
|
|
50
43
|
| ColumnNode
|
|
44
|
+
| DormerNode
|
|
51
45
|
| SlabNode
|
|
52
46
|
| WallNode
|
|
53
47
|
| FenceNode
|
|
@@ -61,8 +55,11 @@ const MOVABLE_TYPES = new Set<string>([
|
|
|
61
55
|
'item',
|
|
62
56
|
'window',
|
|
63
57
|
'door',
|
|
58
|
+
'elevator',
|
|
64
59
|
'ceiling',
|
|
60
|
+
'chimney',
|
|
65
61
|
'column',
|
|
62
|
+
'dormer',
|
|
66
63
|
'slab',
|
|
67
64
|
'wall',
|
|
68
65
|
'fence',
|
|
@@ -77,38 +74,17 @@ function isMovableNode(node: AnyNode | null): node is MovableNode {
|
|
|
77
74
|
return !!node && MOVABLE_TYPES.has(node.type)
|
|
78
75
|
}
|
|
79
76
|
|
|
80
|
-
function panelForType(type: string | null) {
|
|
77
|
+
function panelForType(type: string | null, footer?: React.ReactNode) {
|
|
81
78
|
if (!type) return null
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
case 'stair-segment':
|
|
92
|
-
return <StairSegmentPanel />
|
|
93
|
-
case 'slab':
|
|
94
|
-
return <SlabPanel />
|
|
95
|
-
case 'spawn':
|
|
96
|
-
return <SpawnPanel />
|
|
97
|
-
case 'ceiling':
|
|
98
|
-
return <CeilingPanel />
|
|
99
|
-
case 'column':
|
|
100
|
-
return <ColumnPanel />
|
|
101
|
-
case 'wall':
|
|
102
|
-
return <WallPanel />
|
|
103
|
-
case 'fence':
|
|
104
|
-
return <FencePanel />
|
|
105
|
-
case 'door':
|
|
106
|
-
return <DoorPanel />
|
|
107
|
-
case 'window':
|
|
108
|
-
return <WindowPanel />
|
|
109
|
-
default:
|
|
110
|
-
return null
|
|
111
|
-
}
|
|
79
|
+
// Every kind now renders through `<ParametricInspector>`, which either
|
|
80
|
+
// composes auto-derived editors from `parametrics.groups` or lazy-
|
|
81
|
+
// loads the kind-owned panel via `parametrics.customPanel`. The
|
|
82
|
+
// hardcoded switch is gone — all per-kind panel layout lives in
|
|
83
|
+
// `nodes/src/<kind>/panel.tsx`. The `type` arg is preserved for
|
|
84
|
+
// future cases where we might want a non-registry fallback (e.g.
|
|
85
|
+
// reference scale, paint mode); leave the function shape intact.
|
|
86
|
+
void type
|
|
87
|
+
return <ParametricInspector footer={footer} />
|
|
112
88
|
}
|
|
113
89
|
|
|
114
90
|
function MobilePanelLayer({
|
|
@@ -192,9 +168,11 @@ function MobilePanelLayer({
|
|
|
192
168
|
)
|
|
193
169
|
}
|
|
194
170
|
|
|
195
|
-
export function PanelManager() {
|
|
171
|
+
export function PanelManager({ inspectorFooter }: { inspectorFooter?: React.ReactNode }) {
|
|
196
172
|
const isMobile = useIsMobile()
|
|
197
173
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
174
|
+
const selectedZoneId = useViewer((s) => s.selection.zoneId)
|
|
175
|
+
const setSelection = useViewer((s) => s.setSelection)
|
|
198
176
|
const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
|
|
199
177
|
const isPaintPanelOpen = useEditor((s) => s.isPaintPanelOpen)
|
|
200
178
|
const mode = useEditor((s) => s.mode)
|
|
@@ -239,6 +217,16 @@ export function PanelManager() {
|
|
|
239
217
|
return <PaintPanel />
|
|
240
218
|
}
|
|
241
219
|
|
|
242
|
-
|
|
243
|
-
|
|
220
|
+
if (selectedZoneId && selectedIds.length === 0) {
|
|
221
|
+
return (
|
|
222
|
+
<ParametricInspector
|
|
223
|
+
footer={inspectorFooter}
|
|
224
|
+
key={selectedZoneId}
|
|
225
|
+
nodeId={selectedZoneId as AnyNodeId}
|
|
226
|
+
onClose={() => setSelection({ zoneId: null })}
|
|
227
|
+
/>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return panelForType(selectedNodeType, inspectorFooter)
|
|
244
232
|
}
|