@pascal-app/editor 0.5.1 → 0.7.0
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 -7
- package/src/components/editor/bottom-sheet.tsx +149 -0
- package/src/components/editor/custom-camera-controls.tsx +75 -7
- package/src/components/editor/editor-layout-mobile.tsx +264 -0
- package/src/components/editor/editor-layout-v2.tsx +29 -0
- package/src/components/editor/first-person/build-collider-world.ts +365 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +795 -0
- package/src/components/editor/first-person-controls.tsx +496 -143
- package/src/components/editor/floating-action-menu.tsx +281 -83
- package/src/components/editor/floating-building-action-menu.tsx +4 -3
- package/src/components/editor/floorplan-background-selection.ts +113 -0
- package/src/components/editor/floorplan-panel.tsx +10442 -3275
- package/src/components/editor/index.tsx +270 -20
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/selection-manager.tsx +766 -12
- package/src/components/editor/site-edge-labels.tsx +9 -3
- package/src/components/editor/thumbnail-generator.tsx +350 -157
- package/src/components/editor/use-floorplan-background-placement.ts +257 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
- package/src/components/editor/use-floorplan-scene-data.ts +189 -0
- package/src/components/editor/wall-measurement-label.tsx +377 -58
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
- package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
- package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
- package/src/components/editor-2d/svg-paths.ts +119 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +272 -0
- package/src/components/systems/roof/roof-edit-system.tsx +5 -5
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +2 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +257 -0
- package/src/components/tools/column/column-tool.tsx +97 -0
- package/src/components/tools/column/move-column-tool.tsx +105 -0
- package/src/components/tools/door/door-tool.tsx +19 -0
- package/src/components/tools/door/move-door-tool.tsx +38 -8
- package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
- package/src/components/tools/fence/fence-drafting.ts +27 -8
- package/src/components/tools/fence/fence-tool.tsx +159 -3
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +438 -0
- package/src/components/tools/fence/move-fence-tool.tsx +102 -27
- package/src/components/tools/item/move-tool.tsx +19 -1
- package/src/components/tools/item/placement-math.ts +44 -7
- package/src/components/tools/item/placement-strategies.ts +111 -33
- package/src/components/tools/item/placement-types.ts +7 -0
- package/src/components/tools/item/use-draft-node.ts +2 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +701 -61
- package/src/components/tools/roof/move-roof-tool.tsx +111 -43
- package/src/components/tools/shared/polygon-editor.tsx +244 -29
- package/src/components/tools/shared/segment-angle.ts +156 -0
- package/src/components/tools/slab/move-slab-tool.tsx +182 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +2 -0
- package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
- package/src/components/tools/spawn/spawn-tool.tsx +130 -0
- package/src/components/tools/stair/stair-tool.tsx +11 -3
- package/src/components/tools/tool-manager.tsx +30 -3
- package/src/components/tools/wall/curve-wall-tool.tsx +176 -0
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +423 -0
- package/src/components/tools/wall/move-wall-tool.tsx +356 -0
- package/src/components/tools/wall/wall-drafting.ts +348 -17
- package/src/components/tools/wall/wall-tool.tsx +134 -2
- package/src/components/tools/window/move-window-tool.tsx +28 -0
- package/src/components/tools/window/window-tool.tsx +17 -0
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +37 -5
- package/src/components/ui/action-menu/index.tsx +91 -1
- package/src/components/ui/action-menu/structure-tools.tsx +2 -0
- package/src/components/ui/action-menu/view-toggles.tsx +424 -35
- package/src/components/ui/command-palette/editor-commands.tsx +27 -5
- package/src/components/ui/command-palette/index.tsx +0 -1
- package/src/components/ui/controls/material-picker.tsx +189 -169
- package/src/components/ui/controls/slider-control.tsx +88 -26
- package/src/components/ui/floating-level-selector.tsx +286 -55
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1121 -1219
- package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
- package/src/components/ui/level-duplicate-dialog.tsx +115 -0
- package/src/components/ui/panels/ceiling-panel.tsx +47 -27
- package/src/components/ui/panels/column-panel.tsx +715 -0
- package/src/components/ui/panels/door-panel.tsx +986 -294
- package/src/components/ui/panels/fence-panel.tsx +55 -12
- package/src/components/ui/panels/item-panel.tsx +5 -5
- package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
- package/src/components/ui/panels/node-display.ts +39 -0
- package/src/components/ui/panels/paint-panel.tsx +138 -0
- package/src/components/ui/panels/panel-manager.tsx +241 -30
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +243 -9
- package/src/components/ui/panels/roof-panel.tsx +30 -62
- package/src/components/ui/panels/roof-segment-panel.tsx +8 -23
- package/src/components/ui/panels/slab-panel.tsx +46 -24
- package/src/components/ui/panels/spawn-panel.tsx +155 -0
- package/src/components/ui/panels/stair-panel.tsx +117 -69
- package/src/components/ui/panels/stair-segment-panel.tsx +13 -27
- package/src/components/ui/panels/wall-panel.tsx +71 -17
- package/src/components/ui/panels/window-panel.tsx +665 -146
- package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +138 -56
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +12 -6
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/viewer-toolbar.tsx +96 -2
- package/src/components/viewer-overlay.tsx +25 -19
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-contextual-tools.ts +14 -13
- package/src/hooks/use-keyboard.ts +67 -9
- package/src/hooks/use-mobile.ts +12 -12
- package/src/index.tsx +2 -1
- package/src/lib/door-interaction.ts +88 -0
- package/src/lib/floorplan/geometry.ts +263 -0
- package/src/lib/floorplan/index.ts +38 -0
- package/src/lib/floorplan/items.ts +179 -0
- package/src/lib/floorplan/selection-tool.ts +231 -0
- package/src/lib/floorplan/stairs.ts +478 -0
- package/src/lib/floorplan/types.ts +57 -0
- package/src/lib/floorplan/walls.ts +23 -0
- package/src/lib/guide-events.ts +10 -0
- package/src/lib/history.ts +20 -0
- package/src/lib/level-duplication.test.ts +72 -0
- package/src/lib/level-duplication.ts +153 -0
- package/src/lib/local-guide-image.ts +42 -0
- package/src/lib/material-paint.ts +284 -0
- package/src/lib/roof-duplication.ts +214 -0
- package/src/lib/scene-bounds.test.ts +183 -0
- package/src/lib/scene-bounds.ts +169 -0
- package/src/lib/sfx-player.ts +96 -13
- package/src/lib/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- package/src/store/use-editor.tsx +279 -15
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
closestCenter,
|
|
5
|
+
DndContext,
|
|
6
|
+
type DragEndEvent,
|
|
7
|
+
type DragStartEvent,
|
|
8
|
+
KeyboardSensor,
|
|
9
|
+
PointerSensor,
|
|
10
|
+
useSensor,
|
|
11
|
+
useSensors,
|
|
12
|
+
} from '@dnd-kit/core'
|
|
13
|
+
import {
|
|
14
|
+
arrayMove,
|
|
15
|
+
SortableContext,
|
|
16
|
+
sortableKeyboardCoordinates,
|
|
17
|
+
useSortable,
|
|
18
|
+
verticalListSortingStrategy,
|
|
19
|
+
} from '@dnd-kit/sortable'
|
|
20
|
+
import { CSS } from '@dnd-kit/utilities'
|
|
3
21
|
import {
|
|
4
22
|
type AnyNode,
|
|
5
23
|
type AnyNodeId,
|
|
@@ -8,11 +26,23 @@ import {
|
|
|
8
26
|
useScene,
|
|
9
27
|
} from '@pascal-app/core'
|
|
10
28
|
import { useViewer } from '@pascal-app/viewer'
|
|
11
|
-
import { MoreVertical, Plus, Trash2 } from 'lucide-react'
|
|
12
|
-
import {
|
|
29
|
+
import { Copy, GripVertical, MoreVertical, Plus, Trash2 } from 'lucide-react'
|
|
30
|
+
import {
|
|
31
|
+
type ButtonHTMLAttributes,
|
|
32
|
+
type CSSProperties,
|
|
33
|
+
useCallback,
|
|
34
|
+
useEffect,
|
|
35
|
+
useRef,
|
|
36
|
+
useState,
|
|
37
|
+
} from 'react'
|
|
13
38
|
import { useShallow } from 'zustand/react/shallow'
|
|
39
|
+
import {
|
|
40
|
+
buildLevelDuplicateCreateOps,
|
|
41
|
+
type LevelDuplicatePreset,
|
|
42
|
+
} from '../../lib/level-duplication'
|
|
14
43
|
import { deleteLevelWithFallbackSelection } from '../../lib/level-selection'
|
|
15
44
|
import { cn } from '../../lib/utils'
|
|
45
|
+
import { LevelDuplicateDialog } from './level-duplicate-dialog'
|
|
16
46
|
import {
|
|
17
47
|
Dialog,
|
|
18
48
|
DialogContent,
|
|
@@ -91,14 +121,23 @@ function LevelInlineRename({
|
|
|
91
121
|
function LevelRow({
|
|
92
122
|
level,
|
|
93
123
|
isSelected,
|
|
124
|
+
isDragging,
|
|
125
|
+
dragHandleProps,
|
|
126
|
+
dragHandleRef,
|
|
94
127
|
onSelect,
|
|
128
|
+
onDuplicate,
|
|
95
129
|
onRequestDelete,
|
|
96
130
|
}: {
|
|
97
131
|
level: LevelNode
|
|
98
132
|
isSelected: boolean
|
|
133
|
+
isDragging?: boolean
|
|
134
|
+
dragHandleProps?: ButtonHTMLAttributes<HTMLButtonElement>
|
|
135
|
+
dragHandleRef?: (element: HTMLButtonElement | null) => void
|
|
99
136
|
onSelect: () => void
|
|
137
|
+
onDuplicate: (preset?: LevelDuplicatePreset) => void
|
|
100
138
|
onRequestDelete: () => void
|
|
101
139
|
}) {
|
|
140
|
+
const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false)
|
|
102
141
|
const [isEditing, setIsEditing] = useState(false)
|
|
103
142
|
|
|
104
143
|
return (
|
|
@@ -113,13 +152,32 @@ function LevelRow({
|
|
|
113
152
|
<div
|
|
114
153
|
className={cn(
|
|
115
154
|
'flex items-center rounded-lg transition-colors',
|
|
155
|
+
isDragging && 'bg-white/10 text-foreground shadow-lg',
|
|
116
156
|
isSelected
|
|
117
157
|
? 'bg-white/10 text-foreground'
|
|
118
158
|
: 'text-muted-foreground/70 hover:bg-white/5 hover:text-muted-foreground',
|
|
119
159
|
)}
|
|
120
160
|
>
|
|
121
161
|
<button
|
|
122
|
-
|
|
162
|
+
{...dragHandleProps}
|
|
163
|
+
aria-label={`Reorder ${getLevelDisplayLabel(level)}`}
|
|
164
|
+
className={cn(
|
|
165
|
+
'ml-0.5 flex h-6 w-4 shrink-0 touch-none cursor-grab 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
|
+
isDragging && 'cursor-grabbing opacity-100',
|
|
167
|
+
)}
|
|
168
|
+
onClick={(e) => {
|
|
169
|
+
e.stopPropagation()
|
|
170
|
+
dragHandleProps?.onClick?.(e)
|
|
171
|
+
}}
|
|
172
|
+
ref={dragHandleRef}
|
|
173
|
+
title="Drag to reorder"
|
|
174
|
+
type="button"
|
|
175
|
+
>
|
|
176
|
+
<GripVertical className="h-3.5 w-3.5" />
|
|
177
|
+
</button>
|
|
178
|
+
|
|
179
|
+
<button
|
|
180
|
+
className="flex min-w-0 flex-1 items-center justify-start py-1.5 pr-2 pl-1 font-medium text-xs"
|
|
123
181
|
onClick={onSelect}
|
|
124
182
|
onDoubleClick={(e) => {
|
|
125
183
|
e.stopPropagation()
|
|
@@ -142,7 +200,29 @@ function LevelRow({
|
|
|
142
200
|
<MoreVertical className="h-3 w-3" />
|
|
143
201
|
</button>
|
|
144
202
|
</PopoverTrigger>
|
|
145
|
-
<PopoverContent align="start" className="w-
|
|
203
|
+
<PopoverContent align="start" className="w-44 p-1" side="right" sideOffset={8}>
|
|
204
|
+
<button
|
|
205
|
+
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"
|
|
206
|
+
onClick={(e) => {
|
|
207
|
+
e.stopPropagation()
|
|
208
|
+
onDuplicate()
|
|
209
|
+
}}
|
|
210
|
+
type="button"
|
|
211
|
+
>
|
|
212
|
+
<Copy className="h-3 w-3" />
|
|
213
|
+
Duplicate level
|
|
214
|
+
</button>
|
|
215
|
+
<button
|
|
216
|
+
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"
|
|
217
|
+
onClick={(e) => {
|
|
218
|
+
e.stopPropagation()
|
|
219
|
+
setDuplicateDialogOpen(true)
|
|
220
|
+
}}
|
|
221
|
+
type="button"
|
|
222
|
+
>
|
|
223
|
+
<Copy className="h-3 w-3" />
|
|
224
|
+
Duplicate with options...
|
|
225
|
+
</button>
|
|
146
226
|
<button
|
|
147
227
|
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"
|
|
148
228
|
onClick={(e) => {
|
|
@@ -158,6 +238,62 @@ function LevelRow({
|
|
|
158
238
|
</Popover>
|
|
159
239
|
</div>
|
|
160
240
|
)}
|
|
241
|
+
<LevelDuplicateDialog
|
|
242
|
+
level={level}
|
|
243
|
+
onConfirm={(preset) => {
|
|
244
|
+
onDuplicate(preset)
|
|
245
|
+
setDuplicateDialogOpen(false)
|
|
246
|
+
}}
|
|
247
|
+
onOpenChange={setDuplicateDialogOpen}
|
|
248
|
+
open={duplicateDialogOpen}
|
|
249
|
+
/>
|
|
250
|
+
</div>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function SortableLevelRow({
|
|
255
|
+
level,
|
|
256
|
+
isSelected,
|
|
257
|
+
onSelect,
|
|
258
|
+
onDuplicate,
|
|
259
|
+
onRequestDelete,
|
|
260
|
+
}: {
|
|
261
|
+
level: LevelNode
|
|
262
|
+
isSelected: boolean
|
|
263
|
+
onSelect: () => void
|
|
264
|
+
onDuplicate: (preset?: LevelDuplicatePreset) => void
|
|
265
|
+
onRequestDelete: () => void
|
|
266
|
+
}) {
|
|
267
|
+
const {
|
|
268
|
+
attributes,
|
|
269
|
+
isDragging,
|
|
270
|
+
listeners,
|
|
271
|
+
setActivatorNodeRef,
|
|
272
|
+
setNodeRef,
|
|
273
|
+
transform,
|
|
274
|
+
transition,
|
|
275
|
+
} = useSortable({ id: level.id })
|
|
276
|
+
|
|
277
|
+
const style: CSSProperties = {
|
|
278
|
+
opacity: isDragging ? 0.86 : undefined,
|
|
279
|
+
position: 'relative',
|
|
280
|
+
transform: CSS.Transform.toString(transform),
|
|
281
|
+
transition,
|
|
282
|
+
zIndex: isDragging ? 30 : undefined,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<div ref={setNodeRef} style={style}>
|
|
287
|
+
<LevelRow
|
|
288
|
+
dragHandleProps={{ ...attributes, ...listeners }}
|
|
289
|
+
dragHandleRef={setActivatorNodeRef}
|
|
290
|
+
isDragging={isDragging}
|
|
291
|
+
isSelected={isSelected}
|
|
292
|
+
level={level}
|
|
293
|
+
onDuplicate={onDuplicate}
|
|
294
|
+
onRequestDelete={onRequestDelete}
|
|
295
|
+
onSelect={onSelect}
|
|
296
|
+
/>
|
|
161
297
|
</div>
|
|
162
298
|
)
|
|
163
299
|
}
|
|
@@ -169,9 +305,19 @@ export function FloatingLevelSelector() {
|
|
|
169
305
|
const levelId = useViewer((s) => s.selection.levelId)
|
|
170
306
|
const setSelection = useViewer((s) => s.setSelection)
|
|
171
307
|
const createNode = useScene((s) => s.createNode)
|
|
308
|
+
const createNodes = useScene((s) => s.createNodes)
|
|
172
309
|
const updateNodes = useScene((s) => s.updateNodes)
|
|
173
310
|
|
|
174
311
|
const [deletingLevel, setDeletingLevel] = useState<LevelNode | null>(null)
|
|
312
|
+
const [draggingLevelId, setDraggingLevelId] = useState<string | null>(null)
|
|
313
|
+
const sensors = useSensors(
|
|
314
|
+
useSensor(PointerSensor, {
|
|
315
|
+
activationConstraint: { distance: 4 },
|
|
316
|
+
}),
|
|
317
|
+
useSensor(KeyboardSensor, {
|
|
318
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
319
|
+
}),
|
|
320
|
+
)
|
|
175
321
|
|
|
176
322
|
const resolvedBuildingId = useScene((state) => {
|
|
177
323
|
if (selectedBuildingId) return selectedBuildingId
|
|
@@ -251,9 +397,79 @@ export function FloatingLevelSelector() {
|
|
|
251
397
|
setDeletingLevel(null)
|
|
252
398
|
}, [deletingLevel])
|
|
253
399
|
|
|
400
|
+
const handleDuplicateLevel = useCallback(
|
|
401
|
+
(level: LevelNode, preset: LevelDuplicatePreset = 'everything') => {
|
|
402
|
+
const { createOps, newLevelId, shiftedLevels } = buildLevelDuplicateCreateOps({
|
|
403
|
+
nodes: useScene.getState().nodes,
|
|
404
|
+
level,
|
|
405
|
+
levels,
|
|
406
|
+
preset,
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
if (shiftedLevels.length > 0) {
|
|
410
|
+
updateNodes(
|
|
411
|
+
shiftedLevels.map((shiftedLevel) => ({
|
|
412
|
+
id: shiftedLevel.id as AnyNodeId,
|
|
413
|
+
data: { level: shiftedLevel.level } as Partial<AnyNode>,
|
|
414
|
+
})),
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
createNodes(createOps)
|
|
418
|
+
|
|
419
|
+
setSelection({
|
|
420
|
+
buildingId: resolvedBuildingId ?? undefined,
|
|
421
|
+
levelId: newLevelId as LevelNode['id'],
|
|
422
|
+
})
|
|
423
|
+
},
|
|
424
|
+
[createNodes, levels, resolvedBuildingId, setSelection, updateNodes],
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
428
|
+
setDraggingLevelId(String(event.active.id))
|
|
429
|
+
}, [])
|
|
430
|
+
|
|
431
|
+
const handleDragEnd = useCallback(
|
|
432
|
+
(event: DragEndEvent) => {
|
|
433
|
+
setDraggingLevelId(null)
|
|
434
|
+
|
|
435
|
+
const { active, over } = event
|
|
436
|
+
if (!over || active.id === over.id) return
|
|
437
|
+
|
|
438
|
+
const visualLevels = [...levels].reverse()
|
|
439
|
+
const oldIndex = visualLevels.findIndex((level) => level.id === active.id)
|
|
440
|
+
const newIndex = visualLevels.findIndex((level) => level.id === over.id)
|
|
441
|
+
if (oldIndex === -1 || newIndex === -1) return
|
|
442
|
+
|
|
443
|
+
const reorderedVisualLevels = arrayMove(visualLevels, oldIndex, newIndex)
|
|
444
|
+
const levelNumbersDescending = levels.map((level) => level.level).sort((a, b) => b - a)
|
|
445
|
+
|
|
446
|
+
const updates = reorderedVisualLevels
|
|
447
|
+
.map((level, index) => ({
|
|
448
|
+
id: level.id as AnyNodeId,
|
|
449
|
+
nextLevel: levelNumbersDescending[index],
|
|
450
|
+
data: { level: levelNumbersDescending[index] } as Partial<AnyNode>,
|
|
451
|
+
}))
|
|
452
|
+
.filter(({ id, nextLevel }) => {
|
|
453
|
+
const currentLevel = levels.find((level) => level.id === id)
|
|
454
|
+
return currentLevel?.level !== nextLevel
|
|
455
|
+
})
|
|
456
|
+
.map(({ id, data }) => ({ id, data }))
|
|
457
|
+
|
|
458
|
+
if (updates.length > 0) {
|
|
459
|
+
updateNodes(updates)
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
[levels, updateNodes],
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
const handleDragCancel = useCallback(() => {
|
|
466
|
+
setDraggingLevelId(null)
|
|
467
|
+
}, [])
|
|
468
|
+
|
|
254
469
|
if (levels.length === 0) return null
|
|
255
470
|
|
|
256
471
|
const reversedLevels = [...levels].reverse()
|
|
472
|
+
const sortableLevelIds = reversedLevels.map((level) => level.id)
|
|
257
473
|
|
|
258
474
|
const addButtonClass =
|
|
259
475
|
'absolute left-1/2 z-10 flex h-4 w-4 -translate-x-1/2 items-center justify-center rounded-full border border-border/80 bg-neutral-800 text-muted-foreground/60 shadow-md transition-colors hover:bg-neutral-700 hover:text-foreground'
|
|
@@ -263,61 +479,76 @@ export function FloatingLevelSelector() {
|
|
|
263
479
|
<div className="pointer-events-auto absolute top-14 left-3 z-20">
|
|
264
480
|
<div className="relative">
|
|
265
481
|
{/* Floating + at top edge */}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
482
|
+
{!draggingLevelId && (
|
|
483
|
+
<button
|
|
484
|
+
className={cn(addButtonClass, 'top-0 -translate-y-1/2')}
|
|
485
|
+
onClick={handleAddAbove}
|
|
486
|
+
title="Add level above"
|
|
487
|
+
type="button"
|
|
488
|
+
>
|
|
489
|
+
<Plus className="h-2.5 w-2.5" />
|
|
490
|
+
</button>
|
|
491
|
+
)}
|
|
274
492
|
|
|
275
493
|
{/* Floating + at bottom edge */}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
494
|
+
{!draggingLevelId && (
|
|
495
|
+
<button
|
|
496
|
+
className={cn(addButtonClass, 'bottom-0 translate-y-1/2')}
|
|
497
|
+
onClick={handleAddBelow}
|
|
498
|
+
title="Add level below"
|
|
499
|
+
type="button"
|
|
500
|
+
>
|
|
501
|
+
<Plus className="h-2.5 w-2.5" />
|
|
502
|
+
</button>
|
|
503
|
+
)}
|
|
284
504
|
|
|
285
505
|
{/* Level list */}
|
|
286
|
-
<
|
|
287
|
-
{
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
506
|
+
<DndContext
|
|
507
|
+
collisionDetection={closestCenter}
|
|
508
|
+
onDragCancel={handleDragCancel}
|
|
509
|
+
onDragEnd={handleDragEnd}
|
|
510
|
+
onDragStart={handleDragStart}
|
|
511
|
+
sensors={sensors}
|
|
512
|
+
>
|
|
513
|
+
<SortableContext items={sortableLevelIds} strategy={verticalListSortingStrategy}>
|
|
514
|
+
<div className="flex flex-col gap-0.5 rounded-xl border border-border bg-background/90 p-1 shadow-2xl backdrop-blur-md">
|
|
515
|
+
{reversedLevels.map((level, i) => {
|
|
516
|
+
const isSelected = level.id === levelId
|
|
517
|
+
const sortedIndex = levels.indexOf(level)
|
|
518
|
+
const showGapBelow = i < reversedLevels.length - 1
|
|
519
|
+
|
|
520
|
+
return (
|
|
521
|
+
<div className="relative" key={level.id}>
|
|
522
|
+
<SortableLevelRow
|
|
523
|
+
isSelected={isSelected}
|
|
524
|
+
level={level}
|
|
525
|
+
onDuplicate={(preset) => handleDuplicateLevel(level, preset)}
|
|
526
|
+
onRequestDelete={() => setDeletingLevel(level)}
|
|
527
|
+
onSelect={() =>
|
|
528
|
+
setSelection(
|
|
529
|
+
resolvedBuildingId
|
|
530
|
+
? { buildingId: resolvedBuildingId, levelId: level.id }
|
|
531
|
+
: { levelId: level.id },
|
|
532
|
+
)
|
|
533
|
+
}
|
|
534
|
+
/>
|
|
535
|
+
|
|
536
|
+
{showGapBelow && !draggingLevelId && (
|
|
537
|
+
<button
|
|
538
|
+
className={cn(addButtonClass, 'bottom-0 translate-y-1/2')}
|
|
539
|
+
onClick={() => handleInsertBetween(sortedIndex - 1)}
|
|
540
|
+
title="Insert level here"
|
|
541
|
+
type="button"
|
|
542
|
+
>
|
|
543
|
+
<Plus className="h-2.5 w-2.5" />
|
|
544
|
+
</button>
|
|
545
|
+
)}
|
|
546
|
+
</div>
|
|
547
|
+
)
|
|
548
|
+
})}
|
|
549
|
+
</div>
|
|
550
|
+
</SortableContext>
|
|
551
|
+
</DndContext>
|
|
321
552
|
</div>
|
|
322
553
|
</div>
|
|
323
554
|
|
|
@@ -9,6 +9,7 @@ import { SlabHelper } from './slab-helper'
|
|
|
9
9
|
import { WallHelper } from './wall-helper'
|
|
10
10
|
|
|
11
11
|
export function HelperManager() {
|
|
12
|
+
const mode = useEditor((s) => s.mode)
|
|
12
13
|
const tool = useEditor((s) => s.tool)
|
|
13
14
|
const movingNode = useEditor((state) => state.movingNode)
|
|
14
15
|
|
|
@@ -17,6 +18,10 @@ export function HelperManager() {
|
|
|
17
18
|
return <ItemHelper showEsc />
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
if (mode === 'material-paint') {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
// Show appropriate helper based on current tool
|
|
21
26
|
switch (tool) {
|
|
22
27
|
case 'wall':
|