@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.
Files changed (244) hide show
  1. package/package.json +12 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
  3. package/src/components/editor/custom-camera-controls.tsx +755 -25
  4. package/src/components/editor/editor-layout-mobile.tsx +3 -3
  5. package/src/components/editor/editor-layout-v2.tsx +62 -54
  6. package/src/components/editor/export-manager.tsx +32 -3
  7. package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
  8. package/src/components/editor/first-person/build-collider-world.ts +203 -87
  9. package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
  10. package/src/components/editor/first-person-controls.tsx +836 -31
  11. package/src/components/editor/floating-action-menu.tsx +287 -196
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floorplan-panel.tsx +5837 -12006
  14. package/src/components/editor/grid.tsx +51 -17
  15. package/src/components/editor/group-move-handle.tsx +316 -0
  16. package/src/components/editor/group-rotate-handle.tsx +382 -0
  17. package/src/components/editor/group-transform-shared.test.ts +205 -0
  18. package/src/components/editor/group-transform-shared.ts +256 -0
  19. package/src/components/editor/handles/handle-arrow.tsx +523 -0
  20. package/src/components/editor/handles/use-handle-drag.ts +213 -0
  21. package/src/components/editor/index.tsx +234 -190
  22. package/src/components/editor/measurement-pill.tsx +75 -0
  23. package/src/components/editor/node-arrow-handles.tsx +1453 -0
  24. package/src/components/editor/selection-manager.tsx +435 -158
  25. package/src/components/editor/site-edge-labels.tsx +44 -20
  26. package/src/components/editor/slab-hole-highlights.tsx +466 -0
  27. package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
  28. package/src/components/editor/thumbnail-generator.tsx +27 -64
  29. package/src/components/editor/use-floorplan-background-placement.ts +122 -35
  30. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  31. package/src/components/editor/use-floorplan-scene-data.ts +14 -2
  32. package/src/components/editor/wall-measurement-label.tsx +11 -6
  33. package/src/components/editor/wall-move-side-handles.tsx +855 -0
  34. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  35. package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
  36. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
  37. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  38. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  39. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
  40. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
  41. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  42. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
  43. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  44. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
  45. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  46. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
  47. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
  48. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
  49. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
  50. package/src/components/editor-2d/svg-paths.ts +10 -2
  51. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
  52. package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
  53. package/src/components/systems/roof/roof-edit-system.tsx +86 -12
  54. package/src/components/systems/stair/stair-edit-system.tsx +2 -2
  55. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
  56. package/src/components/systems/zone/zone-system.tsx +0 -0
  57. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  58. package/src/components/tools/elevator/elevator-tool.tsx +349 -0
  59. package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
  60. package/src/components/tools/fence/fence-drafting.ts +64 -22
  61. package/src/components/tools/item/move-tool.tsx +40 -107
  62. package/src/components/tools/item/placement-math.test.ts +10 -0
  63. package/src/components/tools/item/placement-math.ts +33 -9
  64. package/src/components/tools/item/placement-strategies.ts +201 -25
  65. package/src/components/tools/item/placement-types.ts +10 -1
  66. package/src/components/tools/item/use-draft-node.ts +20 -2
  67. package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
  68. package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
  69. package/src/components/tools/roof/roof-tool.tsx +108 -12
  70. package/src/components/tools/select/box-select-state.ts +77 -0
  71. package/src/components/tools/select/box-select-tool.tsx +324 -543
  72. package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
  73. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  74. package/src/components/tools/select/select-candidates.test.ts +109 -0
  75. package/src/components/tools/select/select-candidates.ts +85 -0
  76. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  77. package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
  78. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  79. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  80. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  81. package/src/components/tools/shared/placement-box.tsx +123 -0
  82. package/src/components/tools/shared/polygon-editor.tsx +617 -167
  83. package/src/components/tools/shared/segment-angle.ts +57 -1
  84. package/src/components/tools/site/site-boundary-editor.tsx +464 -12
  85. package/src/components/tools/site/site-flag-model.ts +18 -0
  86. package/src/components/tools/stair/stair-defaults.ts +1 -1
  87. package/src/components/tools/stair/stair-tool.tsx +269 -35
  88. package/src/components/tools/tool-manager.tsx +172 -56
  89. package/src/components/tools/wall/wall-drafting.ts +98 -98
  90. package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
  91. package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
  92. package/src/components/tools/zone/zone-tool.tsx +36 -11
  93. package/src/components/ui/action-menu/action-button.tsx +21 -1
  94. package/src/components/ui/action-menu/camera-actions.tsx +0 -0
  95. package/src/components/ui/action-menu/control-modes.tsx +5 -76
  96. package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
  97. package/src/components/ui/action-menu/index.tsx +33 -196
  98. package/src/components/ui/action-menu/structure-tools.tsx +13 -88
  99. package/src/components/ui/action-menu/view-toggles.tsx +262 -37
  100. package/src/components/ui/command-palette/editor-commands.tsx +24 -17
  101. package/src/components/ui/command-palette/index.tsx +6 -256
  102. package/src/components/ui/controls/material-paint-panel.tsx +83 -0
  103. package/src/components/ui/controls/material-picker.tsx +17 -35
  104. package/src/components/ui/controls/metric-control.tsx +33 -11
  105. package/src/components/ui/floating-level-selector.tsx +48 -11
  106. package/src/components/ui/helpers/helper-manager.tsx +21 -17
  107. package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
  108. package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
  109. package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
  110. package/src/components/ui/level-duplicate-dialog.tsx +5 -6
  111. package/src/components/ui/panels/node-display.ts +1 -0
  112. package/src/components/ui/panels/paint-panel.tsx +66 -41
  113. package/src/components/ui/panels/panel-manager.tsx +33 -74
  114. package/src/components/ui/panels/panel-wrapper.tsx +217 -12
  115. package/src/components/ui/panels/parametric-inspector.tsx +469 -0
  116. package/src/components/ui/panels/reference-panel.tsx +28 -13
  117. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  118. package/src/components/ui/primitives/number-input.tsx +1 -1
  119. package/src/components/ui/primitives/sidebar.tsx +0 -0
  120. package/src/components/ui/scene-loader.tsx +1 -3
  121. package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
  122. package/src/components/ui/sidebar/icon-rail.tsx +0 -0
  123. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  124. package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
  125. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
  126. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
  127. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
  128. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  129. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
  130. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
  131. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
  132. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  133. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
  134. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
  135. package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
  136. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  137. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
  138. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
  139. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
  140. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
  141. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
  142. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  143. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
  144. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
  145. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
  146. package/src/components/ui/sidebar/tab-bar.tsx +61 -1
  147. package/src/components/ui/slider.tsx +1 -1
  148. package/src/components/viewer-overlay.tsx +169 -52
  149. package/src/components/viewer-zone-system.tsx +1 -1
  150. package/src/hooks/use-auto-save.ts +24 -3
  151. package/src/hooks/use-ceiling-events.ts +175 -0
  152. package/src/hooks/use-drag-action.ts +124 -0
  153. package/src/hooks/use-keyboard.ts +125 -62
  154. package/src/hooks/use-selection.ts +64 -0
  155. package/src/index.tsx +243 -4
  156. package/src/lib/constants.ts +6 -2
  157. package/src/lib/editor-api.ts +62 -0
  158. package/src/lib/elevator-support.ts +96 -0
  159. package/src/lib/floorplan/apply-alignment.ts +111 -0
  160. package/src/lib/floorplan/index.ts +7 -0
  161. package/src/lib/floorplan/selection-tool.ts +40 -0
  162. package/src/lib/floorplan/stairs.ts +12 -1
  163. package/src/lib/fresh-planar-placement.test.ts +102 -0
  164. package/src/lib/fresh-planar-placement.ts +63 -0
  165. package/src/lib/history.ts +2 -1
  166. package/src/lib/level-duplication.test.ts +0 -2
  167. package/src/lib/level-duplication.ts +55 -43
  168. package/src/lib/level-name.ts +11 -0
  169. package/src/lib/material-paint.ts +212 -20
  170. package/src/lib/placement-metadata.ts +29 -0
  171. package/src/lib/planar-cursor-placement.test.ts +43 -0
  172. package/src/lib/planar-cursor-placement.ts +42 -0
  173. package/src/lib/roof-duplication.ts +2 -2
  174. package/src/lib/scene-bounds.ts +1 -1
  175. package/src/lib/scene-clipboard.ts +267 -0
  176. package/src/lib/scene.ts +13 -4
  177. package/src/lib/sfx-bus.ts +25 -1
  178. package/src/lib/sfx-player.ts +75 -27
  179. package/src/lib/site-boundary.ts +1 -0
  180. package/src/lib/stair-duplication.ts +2 -2
  181. package/src/lib/stair-levels.test.ts +146 -0
  182. package/src/lib/stair-levels.ts +177 -0
  183. package/src/lib/world-grid-snap.ts +199 -0
  184. package/src/lib/zone-content.ts +130 -0
  185. package/src/store/use-alignment-guides.ts +21 -0
  186. package/src/store/use-editor.tsx +342 -91
  187. package/src/store/use-placement-preview.ts +31 -0
  188. package/src/store/use-wall-move-ghosts.ts +36 -0
  189. package/src/store/use-wall-snap-indicator.ts +32 -0
  190. package/tsconfig.json +2 -1
  191. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  192. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  193. package/src/components/feedback-dialog.tsx +0 -265
  194. package/src/components/pascal-radio.tsx +0 -280
  195. package/src/components/preview-button.tsx +0 -16
  196. package/src/components/tools/building/move-building-tool.tsx +0 -157
  197. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  198. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  199. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  200. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
  201. package/src/components/tools/column/column-tool.tsx +0 -97
  202. package/src/components/tools/column/move-column-tool.tsx +0 -105
  203. package/src/components/tools/door/door-math.ts +0 -110
  204. package/src/components/tools/door/door-tool.tsx +0 -324
  205. package/src/components/tools/door/move-door-tool.tsx +0 -412
  206. package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
  207. package/src/components/tools/fence/fence-tool.tsx +0 -346
  208. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
  209. package/src/components/tools/fence/move-fence-tool.tsx +0 -298
  210. package/src/components/tools/item/item-tool.tsx +0 -26
  211. package/src/components/tools/roof/move-roof-tool.tsx +0 -359
  212. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  213. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  214. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  215. package/src/components/tools/slab/slab-tool.tsx +0 -322
  216. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  217. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  218. package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
  219. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
  220. package/src/components/tools/wall/move-wall-tool.tsx +0 -356
  221. package/src/components/tools/wall/wall-tool.tsx +0 -332
  222. package/src/components/tools/window/move-window-tool.tsx +0 -447
  223. package/src/components/tools/window/window-math.ts +0 -117
  224. package/src/components/tools/window/window-tool.tsx +0 -332
  225. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  226. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  227. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  228. package/src/components/ui/panels/ceiling-panel.tsx +0 -250
  229. package/src/components/ui/panels/column-panel.tsx +0 -715
  230. package/src/components/ui/panels/door-panel.tsx +0 -1292
  231. package/src/components/ui/panels/fence-panel.tsx +0 -227
  232. package/src/components/ui/panels/item-panel.tsx +0 -306
  233. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  234. package/src/components/ui/panels/roof-panel.tsx +0 -230
  235. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  236. package/src/components/ui/panels/slab-panel.tsx +0 -250
  237. package/src/components/ui/panels/spawn-panel.tsx +0 -155
  238. package/src/components/ui/panels/stair-panel.tsx +0 -525
  239. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  240. package/src/components/ui/panels/wall-panel.tsx +0 -177
  241. package/src/components/ui/panels/window-panel.tsx +0 -960
  242. package/src/components/ui/viewer-toolbar.tsx +0 -436
  243. package/src/contexts/presets-context.tsx +0 -121
  244. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -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 = `Level ${level.level}`
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,9 +166,9 @@ function LevelRow({
160
166
  >
161
167
  <button
162
168
  {...dragHandleProps}
163
- aria-label={`Reorder ${getLevelDisplayLabel(level)}`}
169
+ aria-label={`Reorder ${getLevelDisplayName(level)}`}
164
170
  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',
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',
167
173
  )}
168
174
  onClick={(e) => {
@@ -183,10 +189,10 @@ function LevelRow({
183
189
  e.stopPropagation()
184
190
  setIsEditing(true)
185
191
  }}
186
- title={getLevelDisplayLabel(level)}
192
+ title={getLevelDisplayName(level)}
187
193
  type="button"
188
194
  >
189
- <span className="truncate">{getLevelDisplayLabel(level)}</span>
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 ? getLevelDisplayLabel(deletingLevel) : ''}</strong>? All
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,17 +1,21 @@
1
1
  'use client'
2
2
 
3
+ import { nodeRegistry } from '@pascal-app/core'
4
+ import { useIsMobile } from '../../../hooks/use-mobile'
3
5
  import useEditor from '../../../store/use-editor'
4
6
  import { BuildingHelper } from './building-helper'
5
- import { CeilingHelper } from './ceiling-helper'
6
7
  import { ItemHelper } from './item-helper'
8
+ import { RegisteredToolHelper } from './registered-tool-helper'
7
9
  import { RoofHelper } from './roof-helper'
8
- import { SlabHelper } from './slab-helper'
9
- import { WallHelper } from './wall-helper'
10
10
 
11
11
  export function HelperManager() {
12
12
  const mode = useEditor((s) => s.mode)
13
13
  const tool = useEditor((s) => s.tool)
14
14
  const movingNode = useEditor((state) => state.movingNode)
15
+ const isMobile = useIsMobile()
16
+
17
+ // Helpers are keyboard-driven hints (Esc, R, etc.) — irrelevant on touch.
18
+ if (isMobile) return null
15
19
 
16
20
  if (movingNode) {
17
21
  if (movingNode.type === 'building') return <BuildingHelper showRotate />
@@ -22,19 +26,19 @@ export function HelperManager() {
22
26
  return null
23
27
  }
24
28
 
25
- // Show appropriate helper based on current tool
26
- switch (tool) {
27
- case 'wall':
28
- return <WallHelper />
29
- case 'item':
30
- return <ItemHelper />
31
- case 'slab':
32
- return <SlabHelper />
33
- case 'ceiling':
34
- return <CeilingHelper />
35
- case 'roof':
36
- return <RoofHelper />
37
- default:
38
- 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
+ }
39
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
40
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
+ }