@pascal-app/editor 0.6.0 → 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.
Files changed (122) hide show
  1. package/package.json +9 -5
  2. package/src/components/editor/bottom-sheet.tsx +149 -0
  3. package/src/components/editor/custom-camera-controls.tsx +75 -7
  4. package/src/components/editor/editor-layout-mobile.tsx +264 -0
  5. package/src/components/editor/editor-layout-v2.tsx +20 -0
  6. package/src/components/editor/first-person/build-collider-world.ts +365 -0
  7. package/src/components/editor/first-person/bvh-ecctrl.tsx +795 -0
  8. package/src/components/editor/first-person-controls.tsx +496 -143
  9. package/src/components/editor/floating-action-menu.tsx +32 -55
  10. package/src/components/editor/floorplan-background-selection.ts +113 -0
  11. package/src/components/editor/floorplan-panel.tsx +9855 -3298
  12. package/src/components/editor/index.tsx +269 -21
  13. package/src/components/editor/selection-manager.tsx +575 -13
  14. package/src/components/editor/thumbnail-generator.tsx +38 -7
  15. package/src/components/editor/use-floorplan-background-placement.ts +257 -0
  16. package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
  17. package/src/components/editor/use-floorplan-scene-data.ts +189 -0
  18. package/src/components/editor/wall-measurement-label.tsx +267 -36
  19. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
  20. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
  21. package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
  22. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
  23. package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
  24. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
  25. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
  26. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
  27. package/src/components/editor-2d/svg-paths.ts +119 -0
  28. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
  29. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
  30. package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
  31. package/src/components/tools/column/column-tool.tsx +97 -0
  32. package/src/components/tools/column/move-column-tool.tsx +105 -0
  33. package/src/components/tools/door/door-tool.tsx +7 -0
  34. package/src/components/tools/door/move-door-tool.tsx +28 -8
  35. package/src/components/tools/fence/fence-drafting.ts +10 -3
  36. package/src/components/tools/fence/fence-tool.tsx +159 -3
  37. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +129 -18
  38. package/src/components/tools/fence/move-fence-tool.tsx +101 -34
  39. package/src/components/tools/item/move-tool.tsx +10 -1
  40. package/src/components/tools/item/placement-math.ts +30 -1
  41. package/src/components/tools/item/placement-strategies.ts +109 -31
  42. package/src/components/tools/item/placement-types.ts +7 -0
  43. package/src/components/tools/item/use-draft-node.ts +2 -0
  44. package/src/components/tools/item/use-placement-coordinator.tsx +660 -52
  45. package/src/components/tools/roof/move-roof-tool.tsx +22 -15
  46. package/src/components/tools/shared/polygon-editor.tsx +153 -28
  47. package/src/components/tools/shared/segment-angle.ts +156 -0
  48. package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
  49. package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
  50. package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
  51. package/src/components/tools/spawn/spawn-tool.tsx +130 -0
  52. package/src/components/tools/tool-manager.tsx +18 -3
  53. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +121 -20
  54. package/src/components/tools/wall/wall-drafting.ts +18 -9
  55. package/src/components/tools/wall/wall-tool.tsx +134 -2
  56. package/src/components/tools/window/move-window-tool.tsx +18 -0
  57. package/src/components/tools/window/window-tool.tsx +5 -0
  58. package/src/components/ui/action-menu/camera-actions.tsx +37 -33
  59. package/src/components/ui/action-menu/control-modes.tsx +28 -1
  60. package/src/components/ui/action-menu/index.tsx +91 -1
  61. package/src/components/ui/action-menu/structure-tools.tsx +2 -0
  62. package/src/components/ui/action-menu/view-toggles.tsx +424 -35
  63. package/src/components/ui/command-palette/editor-commands.tsx +18 -1
  64. package/src/components/ui/controls/material-picker.tsx +152 -165
  65. package/src/components/ui/controls/slider-control.tsx +66 -18
  66. package/src/components/ui/floating-level-selector.tsx +286 -55
  67. package/src/components/ui/helpers/helper-manager.tsx +5 -0
  68. package/src/components/ui/item-catalog/catalog-items.tsx +1116 -1219
  69. package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
  70. package/src/components/ui/level-duplicate-dialog.tsx +115 -0
  71. package/src/components/ui/panels/ceiling-panel.tsx +1 -25
  72. package/src/components/ui/panels/column-panel.tsx +715 -0
  73. package/src/components/ui/panels/door-panel.tsx +981 -289
  74. package/src/components/ui/panels/fence-panel.tsx +3 -45
  75. package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
  76. package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
  77. package/src/components/ui/panels/node-display.ts +39 -0
  78. package/src/components/ui/panels/paint-panel.tsx +138 -0
  79. package/src/components/ui/panels/panel-manager.tsx +210 -1
  80. package/src/components/ui/panels/panel-wrapper.tsx +48 -39
  81. package/src/components/ui/panels/reference-panel.tsx +238 -5
  82. package/src/components/ui/panels/roof-panel.tsx +4 -105
  83. package/src/components/ui/panels/roof-segment-panel.tsx +0 -25
  84. package/src/components/ui/panels/slab-panel.tsx +4 -30
  85. package/src/components/ui/panels/spawn-panel.tsx +155 -0
  86. package/src/components/ui/panels/stair-panel.tsx +11 -117
  87. package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
  88. package/src/components/ui/panels/wall-panel.tsx +1 -95
  89. package/src/components/ui/panels/window-panel.tsx +660 -139
  90. package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
  91. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
  92. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -2
  93. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
  94. package/src/components/ui/sidebar/panels/site-panel/index.tsx +109 -24
  95. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
  96. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
  97. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +9 -3
  98. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +8 -5
  99. package/src/components/ui/sidebar/tab-bar.tsx +3 -0
  100. package/src/components/ui/viewer-toolbar.tsx +42 -1
  101. package/src/hooks/use-auto-frame.ts +45 -0
  102. package/src/hooks/use-keyboard.ts +64 -7
  103. package/src/hooks/use-mobile.ts +12 -12
  104. package/src/lib/door-interaction.ts +88 -0
  105. package/src/lib/floorplan/geometry.ts +263 -0
  106. package/src/lib/floorplan/index.ts +38 -0
  107. package/src/lib/floorplan/items.ts +179 -0
  108. package/src/lib/floorplan/selection-tool.ts +231 -0
  109. package/src/lib/floorplan/stairs.ts +478 -0
  110. package/src/lib/floorplan/types.ts +57 -0
  111. package/src/lib/floorplan/walls.ts +23 -0
  112. package/src/lib/guide-events.ts +10 -0
  113. package/src/lib/level-duplication.test.ts +72 -0
  114. package/src/lib/level-duplication.ts +153 -0
  115. package/src/lib/local-guide-image.ts +42 -0
  116. package/src/lib/material-paint.ts +284 -0
  117. package/src/lib/roof-duplication.ts +214 -0
  118. package/src/lib/scene-bounds.test.ts +183 -0
  119. package/src/lib/scene-bounds.ts +169 -0
  120. package/src/lib/stair-duplication.ts +126 -0
  121. package/src/lib/window-interaction.ts +86 -0
  122. package/src/store/use-editor.tsx +164 -8
@@ -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 { useCallback, useEffect, useRef, useState } from 'react'
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
- className="flex min-w-0 flex-1 items-center justify-start px-2.5 py-1.5 font-medium text-xs"
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-36 p-1" side="right" sideOffset={8}>
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
- <button
267
- className={cn(addButtonClass, 'top-0 -translate-y-1/2')}
268
- onClick={handleAddAbove}
269
- title="Add level above"
270
- type="button"
271
- >
272
- <Plus className="h-2.5 w-2.5" />
273
- </button>
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
- <button
277
- className={cn(addButtonClass, 'bottom-0 translate-y-1/2')}
278
- onClick={handleAddBelow}
279
- title="Add level below"
280
- type="button"
281
- >
282
- <Plus className="h-2.5 w-2.5" />
283
- </button>
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
- <div className="flex flex-col gap-0.5 rounded-xl border border-border bg-background/90 p-1 shadow-2xl backdrop-blur-md">
287
- {reversedLevels.map((level, i) => {
288
- const isSelected = level.id === levelId
289
- const sortedIndex = levels.indexOf(level)
290
- const showGapBelow = i < reversedLevels.length - 1
291
-
292
- return (
293
- <div className="relative" key={level.id}>
294
- <LevelRow
295
- isSelected={isSelected}
296
- level={level}
297
- onRequestDelete={() => setDeletingLevel(level)}
298
- onSelect={() =>
299
- setSelection(
300
- resolvedBuildingId
301
- ? { buildingId: resolvedBuildingId, levelId: level.id }
302
- : { levelId: level.id },
303
- )
304
- }
305
- />
306
-
307
- {showGapBelow && (
308
- <button
309
- className={cn(addButtonClass, 'bottom-0 translate-y-1/2')}
310
- onClick={() => handleInsertBetween(sortedIndex - 1)}
311
- title="Insert level here"
312
- type="button"
313
- >
314
- <Plus className="h-2.5 w-2.5" />
315
- </button>
316
- )}
317
- </div>
318
- )
319
- })}
320
- </div>
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':