@pascal-app/editor 0.9.1 → 0.9.2

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 (262) hide show
  1. package/package.json +12 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +63 -25
  5. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  6. package/src/components/editor/editor-layout-v2.tsx +18 -0
  7. package/src/components/editor/export-manager.tsx +71 -63
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
  10. package/src/components/editor/first-person/build-collider-world.ts +32 -5
  11. package/src/components/editor/first-person-controls.tsx +118 -7
  12. package/src/components/editor/floating-action-menu.tsx +410 -33
  13. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +1804 -849
  17. package/src/components/editor/grid.tsx +166 -39
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +102 -64
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +192 -1
  24. package/src/components/editor/group-transform-shared.ts +240 -7
  25. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  26. package/src/components/editor/handles/use-handle-drag.ts +37 -4
  27. package/src/components/editor/index.tsx +248 -27
  28. package/src/components/editor/measurement-pill.tsx +62 -22
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +366 -173
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +812 -466
  36. package/src/components/editor/site-edge-labels.tsx +4 -4
  37. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  38. package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +136 -43
  41. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  42. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  43. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  44. package/src/components/editor/wall-measurement-label.tsx +3 -13
  45. package/src/components/editor/wall-move-side-handles.tsx +36 -21
  46. package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
  47. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  48. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
  49. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  50. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  51. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  52. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  53. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  54. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
  55. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
  56. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  57. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  58. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  59. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
  60. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  61. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
  62. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  63. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
  64. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  65. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  66. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  67. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  68. package/src/components/systems/zone/zone-system.tsx +42 -13
  69. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  70. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  71. package/src/components/tools/fence/fence-drafting.ts +80 -8
  72. package/src/components/tools/item/move-tool.tsx +17 -13
  73. package/src/components/tools/item/placement-math.test.ts +13 -1
  74. package/src/components/tools/item/placement-math.ts +28 -2
  75. package/src/components/tools/item/placement-strategies.ts +246 -5
  76. package/src/components/tools/item/placement-types.ts +13 -1
  77. package/src/components/tools/item/use-draft-node.ts +31 -1
  78. package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
  79. package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
  80. package/src/components/tools/roof/roof-tool.tsx +320 -94
  81. package/src/components/tools/select/box-select-state.ts +18 -7
  82. package/src/components/tools/select/box-select-tool.tsx +176 -40
  83. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  84. package/src/components/tools/select/marquee-geometry.ts +155 -0
  85. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  86. package/src/components/tools/select/select-candidates.ts +1 -1
  87. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  88. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  89. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  90. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  91. package/src/components/tools/shared/placement-box.tsx +183 -1
  92. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  93. package/src/components/tools/site/site-boundary-editor.tsx +88 -36
  94. package/src/components/tools/stair/stair-defaults.ts +1 -0
  95. package/src/components/tools/stair/stair-tool.tsx +82 -11
  96. package/src/components/tools/tool-manager.tsx +86 -25
  97. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  98. package/src/components/tools/wall/wall-drafting.ts +180 -103
  99. package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
  100. package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
  101. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  102. package/src/components/tools/zone/zone-tool.tsx +82 -88
  103. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  104. package/src/components/ui/action-menu/control-modes.tsx +40 -93
  105. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  106. package/src/components/ui/action-menu/index.tsx +2 -4
  107. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  108. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  109. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  110. package/src/components/ui/command-palette/editor-commands.tsx +2 -2
  111. package/src/components/ui/command-palette/index.tsx +4 -3
  112. package/src/components/ui/controls/material-paint-panel.tsx +86 -17
  113. package/src/components/ui/controls/material-picker.tsx +83 -152
  114. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  115. package/src/components/ui/controls/metric-control.tsx +118 -44
  116. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  117. package/src/components/ui/controls/slider-control.tsx +88 -38
  118. package/src/components/ui/floating-level-selector.tsx +1 -1
  119. package/src/components/ui/helpers/building-helper.tsx +10 -23
  120. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  121. package/src/components/ui/helpers/helper-manager.tsx +227 -14
  122. package/src/components/ui/helpers/item-helper.tsx +28 -32
  123. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  124. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  125. package/src/components/ui/icon-ref.tsx +47 -0
  126. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  127. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/panels/node-display.ts +17 -17
  130. package/src/components/ui/panels/panel-manager.tsx +22 -13
  131. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  132. package/src/components/ui/panels/parametric-inspector.tsx +15 -2
  133. package/src/components/ui/panels/reference-panel.tsx +54 -19
  134. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  135. package/src/components/ui/primitives/sidebar.tsx +1 -0
  136. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  137. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  138. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  139. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
  140. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
  141. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  142. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  144. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  145. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  146. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  147. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  148. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  149. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  150. package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
  151. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  152. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  153. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  154. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  155. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  156. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  157. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  158. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  159. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  160. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  161. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  162. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  163. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  164. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  165. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  166. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  167. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  168. package/src/components/ui/snap-target-badge.tsx +88 -0
  169. package/src/components/viewer-overlay.tsx +85 -10
  170. package/src/components/viewer-zone-system.tsx +6 -1
  171. package/src/hooks/use-auto-save.test.ts +14 -0
  172. package/src/hooks/use-auto-save.ts +54 -7
  173. package/src/hooks/use-ceiling-events.ts +3 -2
  174. package/src/hooks/use-drag-action.ts +4 -1
  175. package/src/hooks/use-keyboard.ts +385 -40
  176. package/src/index.tsx +219 -6
  177. package/src/lib/active-placement-surface.ts +35 -0
  178. package/src/lib/ceiling-plan-snap.ts +24 -0
  179. package/src/lib/contextual-help.test.ts +112 -0
  180. package/src/lib/contextual-help.ts +144 -0
  181. package/src/lib/continuation.ts +64 -0
  182. package/src/lib/direct-manipulation.test.ts +192 -0
  183. package/src/lib/direct-manipulation.ts +109 -0
  184. package/src/lib/editor-api.ts +23 -35
  185. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  186. package/src/lib/floorplan/apply-alignment.ts +18 -12
  187. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  188. package/src/lib/floorplan/geometry.ts +53 -0
  189. package/src/lib/floorplan/index.ts +3 -0
  190. package/src/lib/floorplan/items.ts +5 -2
  191. package/src/lib/floorplan/plan-coords.ts +21 -0
  192. package/src/lib/fresh-planar-placement.test.ts +240 -3
  193. package/src/lib/fresh-planar-placement.ts +68 -1
  194. package/src/lib/glb-export.test.ts +441 -0
  195. package/src/lib/glb-export.ts +874 -0
  196. package/src/lib/history.ts +9 -0
  197. package/src/lib/interaction/hot-set.test.ts +133 -0
  198. package/src/lib/interaction/hot-set.ts +67 -0
  199. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  200. package/src/lib/interaction/overlay-policy.ts +59 -0
  201. package/src/lib/interaction/scope.ts +211 -0
  202. package/src/lib/level-selection.ts +2 -2
  203. package/src/lib/material-paint.ts +36 -49
  204. package/src/lib/measurement-kind.test.ts +30 -0
  205. package/src/lib/measurement-kind.ts +19 -0
  206. package/src/lib/measurement-label.test.ts +47 -0
  207. package/src/lib/measurement-label.ts +68 -0
  208. package/src/lib/measurement-parser.ts +116 -0
  209. package/src/lib/measurements.test.ts +183 -0
  210. package/src/lib/measurements.ts +199 -0
  211. package/src/lib/paint-scope.test.ts +454 -0
  212. package/src/lib/paint-scope.ts +461 -0
  213. package/src/lib/placement-drag-release.ts +23 -0
  214. package/src/lib/planar-cursor-placement.test.ts +57 -0
  215. package/src/lib/plugin-panels.test.ts +19 -0
  216. package/src/lib/plugin-panels.ts +91 -0
  217. package/src/lib/quick-measurement.test.ts +77 -0
  218. package/src/lib/quick-measurement.ts +109 -0
  219. package/src/lib/roof-duplication.ts +6 -6
  220. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  221. package/src/lib/roof-wall-hit.ts +164 -0
  222. package/src/lib/scene-clipboard.test.ts +196 -0
  223. package/src/lib/scene-clipboard.ts +73 -8
  224. package/src/lib/scene.ts +28 -5
  225. package/src/lib/selection-routing.test.ts +298 -0
  226. package/src/lib/selection-routing.ts +175 -0
  227. package/src/lib/sfx/index.ts +1 -0
  228. package/src/lib/sfx/movement-tick.test.ts +65 -0
  229. package/src/lib/sfx/movement-tick.ts +18 -0
  230. package/src/lib/sfx-bus.ts +50 -13
  231. package/src/lib/sfx-player.test.ts +124 -0
  232. package/src/lib/sfx-player.ts +111 -66
  233. package/src/lib/slab-plan-snap.test.ts +93 -0
  234. package/src/lib/slab-plan-snap.ts +108 -0
  235. package/src/lib/snapping-mode.test.ts +138 -0
  236. package/src/lib/snapping-mode.ts +179 -0
  237. package/src/lib/stair-duplication.ts +4 -4
  238. package/src/lib/surface-plan-snap.test.ts +71 -0
  239. package/src/lib/surface-plan-snap.ts +256 -0
  240. package/src/lib/use-linear-display.ts +40 -0
  241. package/src/lib/world-grid-snap.ts +37 -5
  242. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  243. package/src/store/use-editor.tsx +484 -114
  244. package/src/store/use-facing-pose.ts +44 -0
  245. package/src/store/use-fence-curve-draft.ts +21 -0
  246. package/src/store/use-floorplan-draft-preview.ts +101 -0
  247. package/src/store/use-floorplan-marquee.ts +50 -0
  248. package/src/store/use-interaction-scope.test.ts +186 -0
  249. package/src/store/use-interaction-scope.ts +132 -0
  250. package/src/store/use-measurement-draft.test.ts +530 -0
  251. package/src/store/use-measurement-draft.ts +543 -0
  252. package/src/store/use-opening-guides.ts +41 -0
  253. package/src/store/use-placement-preview.ts +11 -3
  254. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  255. package/src/store/use-quick-measurement-hud.ts +77 -0
  256. package/src/store/use-segment-draft-chain.ts +28 -0
  257. package/src/store/use-stair-build-preview.ts +43 -0
  258. package/src/store/use-wall-snap-indicator.ts +2 -0
  259. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  260. package/src/components/editor/group-move-handle.tsx +0 -316
  261. package/src/components/ui/panels/paint-panel.tsx +0 -163
  262. package/src/lib/level-name.ts +0 -11
@@ -1,44 +1,257 @@
1
1
  'use client'
2
2
 
3
- import { nodeRegistry } from '@pascal-app/core'
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ nodeRegistry,
7
+ useScene,
8
+ } from '@pascal-app/core'
9
+ import { useViewer } from '@pascal-app/viewer'
10
+ import { useEffect, useMemo, useState } from 'react'
11
+ import { useShallow } from 'zustand/react/shallow'
4
12
  import { useIsMobile } from '../../../hooks/use-mobile'
5
- import useEditor from '../../../store/use-editor'
13
+ import {
14
+ type ContextualShortcutHint,
15
+ GROUP_MOVE_DRAG_LABEL,
16
+ GROUP_ROTATE_DRAG_LABEL,
17
+ ROTATE_HANDLE_DRAG_LABEL,
18
+ resolveRotateHandleHelpHints,
19
+ resolveSelectModeHelpHints,
20
+ } from '../../../lib/contextual-help'
21
+ import { continuationContextOf } from '../../../lib/continuation'
22
+ import { canDirectMoveNode, canDirectRotateNode } from '../../../lib/direct-manipulation'
23
+ import type { ReshapeKind } from '../../../lib/interaction/scope'
24
+ import { isFreshPlacementMetadata } from '../../../lib/placement-metadata'
25
+ import { snapContextOf } from '../../../lib/snapping-mode'
26
+ import useEditor, { getActiveContinuationContext } from '../../../store/use-editor'
27
+ import useInteractionScope, {
28
+ useActiveHandleDrag,
29
+ useMovingNode,
30
+ } from '../../../store/use-interaction-scope'
6
31
  import { BuildingHelper } from './building-helper'
32
+ import { ContextualHelperPanel } from './contextual-helper-panel'
7
33
  import { ItemHelper } from './item-helper'
8
34
  import { RegisteredToolHelper } from './registered-tool-helper'
9
35
  import { RoofHelper } from './roof-helper'
10
36
 
37
+ // Reshaping a selected node's geometry (endpoint / curve / polygon corner). The
38
+ // snapping chip is the main control; these just name the gesture + Esc.
39
+ function reshapingHints(reshape: ReshapeKind): ContextualShortcutHint[] {
40
+ const action =
41
+ reshape === 'curve'
42
+ ? 'Curve'
43
+ : reshape === 'control-point'
44
+ ? 'Move control point'
45
+ : reshape === 'tangent'
46
+ ? 'Move tangent'
47
+ : reshape === 'endpoint'
48
+ ? 'Move endpoint'
49
+ : 'Move corner'
50
+ return [
51
+ { keys: ['Drag'], label: action },
52
+ { keys: ['Esc'], label: 'Cancel' },
53
+ ]
54
+ }
55
+
56
+ type ActiveModifierKeys = {
57
+ command: boolean
58
+ shift: boolean
59
+ }
60
+
61
+ function useActiveModifierKeys(): ActiveModifierKeys {
62
+ const [modifiers, setModifiers] = useState<ActiveModifierKeys>({
63
+ command: false,
64
+ shift: false,
65
+ })
66
+
67
+ useEffect(() => {
68
+ const updateModifiers = (event: KeyboardEvent) => {
69
+ const isKeyDown = event.type === 'keydown'
70
+ setModifiers({
71
+ command:
72
+ event.metaKey ||
73
+ event.ctrlKey ||
74
+ (isKeyDown && (event.key === 'Meta' || event.key === 'Control')),
75
+ shift: event.shiftKey || (isKeyDown && event.key === 'Shift'),
76
+ })
77
+ }
78
+ const clearModifiers = () => {
79
+ setModifiers({ command: false, shift: false })
80
+ }
81
+
82
+ window.addEventListener('keydown', updateModifiers)
83
+ window.addEventListener('keyup', updateModifiers)
84
+ window.addEventListener('blur', clearModifiers)
85
+ return () => {
86
+ window.removeEventListener('keydown', updateModifiers)
87
+ window.removeEventListener('keyup', updateModifiers)
88
+ window.removeEventListener('blur', clearModifiers)
89
+ }
90
+ }, [])
91
+
92
+ return modifiers
93
+ }
94
+
11
95
  export function HelperManager() {
12
96
  const mode = useEditor((s) => s.mode)
13
97
  const tool = useEditor((s) => s.tool)
14
- const movingNode = useEditor((state) => state.movingNode)
98
+ const measurementToolKind = useEditor((s) => s.toolDefaults.measurement?.kind)
99
+ const workspaceMode = useEditor((s) => s.workspaceMode)
100
+ const scope = useInteractionScope((s) => s.scope)
101
+ const movingNode = useMovingNode()
102
+ const activeHandleDrag = useActiveHandleDrag()
103
+ const selectedIds = useViewer((s) => s.selection.selectedIds)
15
104
  const isMobile = useIsMobile()
105
+ const modifiers = useActiveModifierKeys()
106
+ const selectedNodes = useScene(
107
+ useShallow((s) =>
108
+ selectedIds
109
+ .map((id) => s.nodes[id as AnyNodeId])
110
+ .filter((node): node is AnyNode => node !== undefined),
111
+ ),
112
+ )
113
+ // The snapping context for whatever's active (wall / item / polygon) — drives
114
+ // which snapping chips the HUD shows, derived once and shared by every branch.
115
+ const snapContext = useMemo(
116
+ () =>
117
+ snapContextOf({
118
+ scope,
119
+ mode,
120
+ tool,
121
+ profileOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapProfile,
122
+ draftDirectionalOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapDraftDirectional ?? true,
123
+ }),
124
+ [scope, mode, tool],
125
+ )
126
+ const continuationContext = useMemo(
127
+ () => getActiveContinuationContext(),
128
+ [scope, mode, tool],
129
+ )
130
+ const selectModeHints = useMemo(() => {
131
+ const single = selectedNodes.length === 1 ? selectedNodes[0] : null
132
+ const mepSelection =
133
+ single?.type === 'duct-segment' || single?.type === 'pipe-segment'
134
+ ? 'run'
135
+ : single?.type === 'duct-fitting' || single?.type === 'pipe-fitting'
136
+ ? 'fitting'
137
+ : null
138
+ return resolveSelectModeHelpHints({
139
+ selectedCount: selectedNodes.length,
140
+ hasMovableSelection: selectedNodes.some((node) => canDirectMoveNode(node)),
141
+ hasRotatableSelection: selectedNodes.some((node) => canDirectRotateNode(node)),
142
+ commandPressed: modifiers.command,
143
+ shiftPressed: modifiers.shift,
144
+ mepSelection,
145
+ })
146
+ }, [modifiers.command, modifiers.shift, selectedNodes])
16
147
 
17
148
  // Helpers are keyboard-driven hints (Esc, R, etc.) — irrelevant on touch.
18
149
  if (isMobile) return null
19
150
 
151
+ // The studio workspace (compose panel / gallery) has no scene selection or
152
+ // tools — editor shortcut hints would only mislead there.
153
+ if (workspaceMode === 'studio') return null
154
+
155
+ // Rotating a node (or a multi-selection group) via its in-world gizmo:
156
+ // advertise Shift = free rotation, the same angle-step bypass wall drafting
157
+ // exposes. Takes priority over the idle select-mode hints since a handle
158
+ // drag is the active interaction.
159
+ if (
160
+ activeHandleDrag?.label === ROTATE_HANDLE_DRAG_LABEL ||
161
+ activeHandleDrag?.label === GROUP_ROTATE_DRAG_LABEL
162
+ ) {
163
+ return <ContextualHelperPanel hints={resolveRotateHandleHelpHints(modifiers.shift)} />
164
+ }
165
+
166
+ // Group-move drag / pick-up: the drag resolves to the 'item' snap context
167
+ // (see `snapContextOf`), so surface the snapping chips — mode + grid step,
168
+ // with their Shift / Ctrl cycle shortcuts — plus the mid-move R/T rotate.
169
+ if (activeHandleDrag?.label === GROUP_MOVE_DRAG_LABEL) {
170
+ return (
171
+ <ContextualHelperPanel
172
+ hints={[{ keys: ['R / T'], label: 'Rotate the selection ±45°' }]}
173
+ snapContext={snapContext}
174
+ />
175
+ )
176
+ }
177
+
178
+ // Reshaping a node's geometry (endpoint / curve / polygon corner). Checked
179
+ // before the select branch so the idle "drag selected / add objects" hints
180
+ // never leak over an in-progress reshape — and it gets its own snapping chip.
181
+ if (scope.kind === 'reshaping') {
182
+ return <ContextualHelperPanel hints={reshapingHints(scope.reshape)} snapContext={snapContext} />
183
+ }
184
+
20
185
  if (movingNode) {
21
186
  if (movingNode.type === 'building') return <BuildingHelper showRotate />
22
- return <ItemHelper showEsc />
187
+ // A fresh placement (e.g. a positioned preset like a shelf) advertises its
188
+ // once/repeat continuation, exactly like the GLB item tool — but an existing
189
+ // node being *moved* is not a placement, so it gets no continuation chip.
190
+ const movingContinuationContext = isFreshPlacementMetadata(movingNode.metadata)
191
+ ? continuationContextOf(movingNode.type)
192
+ : null
193
+ // Force-place only makes sense for kinds that collision-validate their drop;
194
+ // structural kinds (wall/slab/…) never reject, so don't advertise Alt.
195
+ return (
196
+ <ItemHelper
197
+ continuationContext={movingContinuationContext}
198
+ showEsc
199
+ showForce={nodeRegistry.get(movingNode.type)?.snapProfile !== 'structural'}
200
+ snapContext={snapContext}
201
+ />
202
+ )
23
203
  }
24
204
 
205
+ // Paint mode advertises (and cycles, via Shift) the application scope — the
206
+ // only contextual control here. The chip hides itself for targets that only
207
+ // paint one surface, so this renders nothing until a scoped target is active.
25
208
  if (mode === 'material-paint') {
26
- return null
209
+ return <ContextualHelperPanel hints={[]} showPaintScope />
27
210
  }
28
211
 
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.
212
+ // Idle select only an active scope (handle-drag, box-select, …) must not show
213
+ // the idle selection hints.
214
+ if (mode === 'select' && scope.kind === 'idle') {
215
+ return <ContextualHelperPanel hints={selectModeHints} />
216
+ }
217
+
218
+ if (tool === 'measurement' && measurementToolKind === 'smart') {
219
+ return (
220
+ <ContextualHelperPanel
221
+ hints={[
222
+ { keys: ['Hover'], label: 'Inspect surface dimensions' },
223
+ { keys: ['Click'], label: 'Pin measurement lens' },
224
+ { keys: ['Esc'], label: 'Exit smart measure' },
225
+ ]}
226
+ />
227
+ )
228
+ }
229
+
230
+ // Legacy fallback — only `roof` remains because it hasn't migrated to
231
+ // `def.tool` / `def.toolHints` yet (no Stage D port). Checked before the
232
+ // generic tool branch so the snap-context fallback below doesn't capture it
233
+ // and drop its bespoke `RoofHelper` hints. When roof migrates, this deletes.
234
+ if (tool === 'roof') return <RoofHelper snapContext={snapContext} />
235
+
236
+ // Registry-first: a kind renders the generic `RegisteredToolHelper` when it
237
+ // declares `def.toolHints`, OR whenever its draft resolves to a snap /
238
+ // continuation context — so a snappable tool with NO hand-written hints (e.g.
239
+ // `zone`) still advertises the snapping chip it already honors (Shift = cycle).
240
+ // `RegisteredToolHelper` self-hides when there's genuinely nothing to show.
32
241
  if (tool) {
33
242
  const def = nodeRegistry.get(tool)
34
- if (def?.toolHints && def.toolHints.length > 0) {
35
- return <RegisteredToolHelper hints={def.toolHints} />
243
+ const hints = def?.toolHints ?? []
244
+ if (hints.length > 0 || snapContext || continuationContext) {
245
+ return (
246
+ <RegisteredToolHelper
247
+ continuationContext={continuationContext}
248
+ hints={hints}
249
+ shiftPressed={modifiers.shift}
250
+ snapContext={snapContext}
251
+ />
252
+ )
36
253
  }
37
254
  }
38
255
 
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
256
  return null
44
257
  }
@@ -1,40 +1,36 @@
1
- import { ShortcutToken } from '../primitives/shortcut-token'
1
+ import type { ContinuationContext } from '../../../lib/continuation'
2
+ import type { SnapContext } from '../../../lib/snapping-mode'
3
+ import { ContextualHelperPanel } from './contextual-helper-panel'
2
4
 
3
5
  interface ItemHelperProps {
4
6
  showEsc?: boolean
7
+ snapContext?: SnapContext | null
8
+ // Whether to advertise Alt = force-place. Only meaningful for kinds that
9
+ // collision-validate their drop (structural kinds never reject, so it's hidden).
10
+ showForce?: boolean
11
+ // Set for a fresh point-kind placement (e.g. a positioned preset) so the
12
+ // once/repeat continuation chip shows; null for an existing-node move.
13
+ continuationContext?: ContinuationContext | null
5
14
  }
6
15
 
7
- export function ItemHelper({ showEsc }: ItemHelperProps) {
16
+ // Snapping mode is the chip on the right (Shift cycles it), so it's not repeated
17
+ // as a key hint. Rotate is the two keys; Alt forces an invalid (red) drop.
18
+ export function ItemHelper({
19
+ showEsc,
20
+ snapContext,
21
+ showForce,
22
+ continuationContext = null,
23
+ }: ItemHelperProps) {
8
24
  return (
9
- <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">
10
- <div className="flex items-center gap-2 text-sm">
11
- <ShortcutToken value="Left click" />
12
- <span className="text-muted-foreground">Place item</span>
13
- </div>
14
- <div className="flex items-center gap-2 text-sm">
15
- <ShortcutToken value="R" />
16
- <span className="text-muted-foreground">Rotate counterclockwise</span>
17
- </div>
18
- <div className="flex items-center gap-2 text-sm">
19
- <ShortcutToken value="T" />
20
- <span className="text-muted-foreground">Rotate clockwise</span>
21
- </div>
22
- <div className="flex items-center gap-2 text-sm">
23
- <ShortcutToken value="Shift" />
24
- <span className="text-muted-foreground">Free place</span>
25
- </div>
26
- {showEsc && (
27
- <div className="flex items-center gap-2 text-sm">
28
- <ShortcutToken value="Esc" />
29
- <span className="text-muted-foreground">Cancel</span>
30
- </div>
31
- )}
32
- {!showEsc && (
33
- <div className="flex items-center gap-2 text-sm">
34
- <ShortcutToken value="Right click" />
35
- <span className="text-muted-foreground">Cancel</span>
36
- </div>
37
- )}
38
- </div>
25
+ <ContextualHelperPanel
26
+ continuationContext={continuationContext}
27
+ hints={[
28
+ { keys: ['Left click'], label: 'Place' },
29
+ { keys: ['R', 'T'], label: 'Rotate' },
30
+ ...(showForce ? [{ keys: ['Alt'], label: 'Force place' }] : []),
31
+ { keys: [showEsc ? 'Esc' : 'Right click'], label: 'Cancel' },
32
+ ]}
33
+ snapContext={snapContext}
34
+ />
39
35
  )
40
36
  }
@@ -1,5 +1,8 @@
1
1
  import type { ToolHint } from '@pascal-app/core'
2
- import { ShortcutToken } from '../primitives/shortcut-token'
2
+ import type { ContinuationContext } from '../../../lib/continuation'
3
+ import type { SnapContext } from '../../../lib/snapping-mode'
4
+ import useEditor from '../../../store/use-editor'
5
+ import { ContextualHelperPanel } from './contextual-helper-panel'
3
6
 
4
7
  /**
5
8
  * Generic helper panel rendered from `def.toolHints` data. Matches the
@@ -10,16 +13,47 @@ import { ShortcutToken } from '../primitives/shortcut-token'
10
13
  * Drops the need for per-kind helper files entirely — kinds declare
11
14
  * their hints as static data in their `NodeDefinition`.
12
15
  */
13
- export function RegisteredToolHelper({ hints }: { hints: ToolHint[] }) {
14
- if (hints.length === 0) return null
16
+ export function RegisteredToolHelper({
17
+ hints,
18
+ shiftPressed = false,
19
+ snapContext = null,
20
+ continuationContext = null,
21
+ }: {
22
+ hints: ToolHint[]
23
+ shiftPressed?: boolean
24
+ snapContext?: SnapContext | null
25
+ continuationContext?: ContinuationContext | null
26
+ }) {
27
+ // Live vertex count of an in-progress polygon draft, so hints gated on a
28
+ // minimum (e.g. "Finish" at ≥ 3) only appear once they're actually possible.
29
+ const draftVertexCount = useEditor((s) => s.draftVertexCount)
30
+ // Some hints are replaced by live contextual chips, so keep the generic
31
+ // registry renderer from duplicating stale/static versions.
32
+ const visible = hints.filter(
33
+ (hint) =>
34
+ !(hint.key === 'Shift' && hint.label === 'Cycle snapping mode') &&
35
+ (hint.minDraftVertices == null || draftVertexCount >= hint.minDraftVertices),
36
+ )
37
+ if (visible.length === 0 && !snapContext && !continuationContext) return null
38
+ // Hints carrying a live-state `chip` render as mode chips next to the
39
+ // snapping / continuation rows; the rest stay static key rows.
40
+ const chipHints = visible.filter((hint) => hint.chip)
41
+ const staticHints = visible.filter((hint) => !hint.chip)
15
42
  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>
43
+ <ContextualHelperPanel
44
+ chipHints={chipHints}
45
+ hints={staticHints.map((hint) => {
46
+ // Shift is a per-kind bypass for opening / zone / duct placement ("Free
47
+ // place", "Free angle", …) — those flip to a bypassed state while held.
48
+ const isBypassHint = hint.key === 'Shift'
49
+ return {
50
+ keys: [hint.key],
51
+ label: shiftPressed && isBypassHint ? 'Guided constraints bypassed' : hint.label,
52
+ active: shiftPressed && isBypassHint,
53
+ }
54
+ })}
55
+ continuationContext={continuationContext}
56
+ snapContext={snapContext}
57
+ />
24
58
  )
25
59
  }
@@ -1,16 +1,14 @@
1
- import { ShortcutToken } from '../primitives/shortcut-token'
1
+ import type { SnapContext } from '../../../lib/snapping-mode'
2
+ import { ContextualHelperPanel } from './contextual-helper-panel'
2
3
 
3
- export function RoofHelper() {
4
+ export function RoofHelper({ snapContext }: { snapContext?: SnapContext | null }) {
4
5
  return (
5
- <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">
6
- <div className="flex items-center gap-2 text-sm">
7
- <ShortcutToken value="Left click" />
8
- <span className="text-muted-foreground">Set corner</span>
9
- </div>
10
- <div className="flex items-center gap-2 text-sm">
11
- <ShortcutToken value="Esc" />
12
- <span className="text-muted-foreground">Cancel</span>
13
- </div>
14
- </div>
6
+ <ContextualHelperPanel
7
+ hints={[
8
+ { keys: ['Left click'], label: 'Set corner' },
9
+ { keys: ['Esc'], label: 'Cancel' },
10
+ ]}
11
+ snapContext={snapContext}
12
+ />
15
13
  )
16
14
  }
@@ -0,0 +1,47 @@
1
+ 'use client'
2
+
3
+ import { Icon } from '@iconify/react'
4
+ import type { IconRef } from '@pascal-app/core'
5
+ import { type ComponentType, lazy, Suspense } from 'react'
6
+
7
+ // `React.lazy` must be called once per loader so the resolved component keeps
8
+ // a stable identity across renders (otherwise every parent re-render remounts
9
+ // the icon). Cache by the loader function — same pattern as the plugin-panel
10
+ // component cache.
11
+ const lazyIconCache = new WeakMap<() => Promise<{ default: ComponentType }>, ComponentType>()
12
+
13
+ function resolveLazyIcon(module: () => Promise<{ default: ComponentType }>): ComponentType {
14
+ const cached = lazyIconCache.get(module)
15
+ if (cached) return cached
16
+ const Lazy = lazy(module)
17
+ lazyIconCache.set(module, Lazy)
18
+ return Lazy
19
+ }
20
+
21
+ /**
22
+ * Generic renderer for a registry {@link IconRef} — url / iconify / inline-svg
23
+ * marks are sized by `size` (px); `component`-kind icons size themselves.
24
+ * Shared by the quick-action menus; the icon rail and inspector keep their
25
+ * own copies with bespoke wrappers for now.
26
+ */
27
+ export function IconRefGlyph({ icon, size = 16 }: { icon: IconRef; size?: number }) {
28
+ if (icon.kind === 'url') {
29
+ return <img alt="" className="shrink-0 object-contain" height={size} src={icon.src} width={size} />
30
+ }
31
+ if (icon.kind === 'iconify') {
32
+ return <Icon height={size} icon={icon.name} width={size} />
33
+ }
34
+ if (icon.kind === 'svg') {
35
+ return (
36
+ <svg height={size} viewBox={icon.viewBox} width={size}>
37
+ <path d={icon.path} fill="currentColor" />
38
+ </svg>
39
+ )
40
+ }
41
+ const LazyIcon = resolveLazyIcon(icon.module)
42
+ return (
43
+ <Suspense fallback={null}>
44
+ <LazyIcon />
45
+ </Suspense>
46
+ )
47
+ }
@@ -1,6 +1,14 @@
1
1
  import type { AssetInput } from '@pascal-app/core'
2
2
 
3
- export const CATALOG_ITEMS: AssetInput[] = [
3
+ /**
4
+ * A catalog tile: the asset plus optional editor placement metadata.
5
+ * `tool` names the placement tool the tile arms (defaults to the generic
6
+ * `'item'` drop tool) — kinds drawn by their own registry tool (e.g. the
7
+ * modular cabinet) point at that tool id instead.
8
+ */
9
+ export type CatalogItem = AssetInput & { tool?: string }
10
+
11
+ export const CATALOG_ITEMS: CatalogItem[] = [
4
12
  {
5
13
  id: 'cactus',
6
14
  category: 'furniture',
@@ -437,6 +445,36 @@ export const CATALOG_ITEMS: AssetInput[] = [
437
445
  scale: [1, 1, 1],
438
446
  surface: { height: 0.48 },
439
447
  },
448
+ {
449
+ id: 'cabinet',
450
+ tool: 'cabinet',
451
+ category: 'furniture',
452
+ name: 'Modular Cabinet',
453
+ tags: [
454
+ 'cabinet',
455
+ 'cupboard',
456
+ 'storage',
457
+ 'kitchen',
458
+ 'pantry',
459
+ 'wood',
460
+ 'timber',
461
+ 'modern',
462
+ 'contemporary',
463
+ 'minimalist',
464
+ 'organization',
465
+ 'furniture',
466
+ ],
467
+ thumbnail:
468
+ 'https://byrpxoiotywskoojsrzd.supabase.co/storage/v1/object/public/items/system/kitchen-cabinet/thumbnail.png',
469
+ src: 'https://byrpxoiotywskoojsrzd.supabase.co/storage/v1/object/public/items/system/kitchen-cabinet/model.glb',
470
+ floorPlanUrl:
471
+ 'https://byrpxoiotywskoojsrzd.supabase.co/storage/v1/object/public/items/system/kitchen-cabinet/floor-plan.png',
472
+ dimensions: [1.65, 1.09, 0.77],
473
+ offset: [0, 0.0004, 0],
474
+ rotation: [0, 0, 0],
475
+ scale: [1, 1, 1],
476
+ surface: { height: 1.09 },
477
+ },
440
478
  {
441
479
  id: 'sofa',
442
480
  category: 'furniture',
@@ -1,18 +1,13 @@
1
1
  'use client'
2
2
 
3
3
  import type { AssetInput } from '@pascal-app/core'
4
- import { resolveCdnUrl } from '@pascal-app/viewer'
5
- import Image from 'next/image'
4
+ import { resolveCdnUrl, useViewer } from '@pascal-app/viewer'
6
5
  import { useEffect } from 'react'
7
- import {
8
- Tooltip,
9
- TooltipContent,
10
- TooltipTrigger,
11
- } from './../../../components/ui/primitives/tooltip'
12
6
  import { triggerSFX } from './../../../lib/sfx-bus'
13
7
  import { cn } from './../../../lib/utils'
14
8
  import useEditor, { type CatalogCategory } from './../../../store/use-editor'
15
- import { CATALOG_ITEMS } from './catalog-items'
9
+ import { resolveAssetSnapTarget, SnapTargetBadge } from '../snap-target-badge'
10
+ import { CATALOG_ITEMS, type CatalogItem } from './catalog-items'
16
11
 
17
12
  export function ItemCatalog({
18
13
  category,
@@ -41,9 +36,9 @@ export function ItemCatalog({
41
36
  const setMode = useEditor((state) => state.setMode)
42
37
  const setTool = useEditor((state) => state.setTool)
43
38
 
44
- const sourceItems = itemsOverride ?? CATALOG_ITEMS
39
+ const sourceItems: CatalogItem[] = itemsOverride ?? CATALOG_ITEMS
45
40
  // Server-provided results bypass all local filtering; otherwise filter by category/search/tags
46
- const filteredItems =
41
+ const filteredItems: CatalogItem[] =
47
42
  overrideItems ??
48
43
  (() => {
49
44
  const categoryItems = search
@@ -58,22 +53,6 @@ export function ItemCatalog({
58
53
  })
59
54
  })()
60
55
 
61
- const categoryItems = filteredItems
62
-
63
- // Auto-select first item if current selection is not in the filtered list
64
- useEffect(() => {
65
- const isCurrentItemInCategory = categoryItems.some((item) => item.src === selectedItem?.src)
66
- if (!isCurrentItemInCategory && categoryItems.length > 0) {
67
- setSelectedItem(categoryItems[0] as AssetInput)
68
- }
69
- }, [categoryItems, selectedItem?.src, setSelectedItem])
70
-
71
- const getAttachmentIcon = (attachTo: AssetInput['attachTo']) => {
72
- if (attachTo === 'wall' || attachTo === 'wall-side') return '/icons/wall.png'
73
- if (attachTo === 'ceiling') return '/icons/ceiling.png'
74
- return null
75
- }
76
-
77
56
  if (filteredItems.length === 0 && emptyState) {
78
57
  return <>{emptyState}</>
79
58
  }
@@ -86,7 +65,7 @@ export function ItemCatalog({
86
65
  {leadingTile}
87
66
  {filteredItems.map((item, index) => {
88
67
  const isSelected = selectedItem?.src === item?.src
89
- const attachmentIcon = getAttachmentIcon(item?.attachTo)
68
+ const snapTarget = resolveAssetSnapTarget(item?.attachTo)
90
69
  return (
91
70
  <button
92
71
  className={cn(
@@ -96,8 +75,12 @@ export function ItemCatalog({
96
75
  key={index}
97
76
  onClick={() => {
98
77
  triggerSFX('sfx:menu-click')
78
+ // Drop the current selection before arming placement — keeping
79
+ // it would route shortcuts (rotate & co) to both the ghost and
80
+ // the selected node.
81
+ useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
99
82
  setSelectedItem(item)
100
- setTool('item')
83
+ setTool(item.tool ?? 'item')
101
84
  setMode('build')
102
85
  }}
103
86
  onMouseEnter={() => triggerSFX('sfx:menu-hover')}
@@ -110,14 +93,8 @@ export function ItemCatalog({
110
93
  loading="eager"
111
94
  src={resolveCdnUrl(item.thumbnail) || ''}
112
95
  />
113
- {attachmentIcon && (
114
- <div className="absolute right-1 bottom-1 flex h-4 w-4 items-center justify-center rounded bg-black/60">
115
- <img
116
- alt={item.attachTo === 'ceiling' ? 'Ceiling attachment' : 'Wall attachment'}
117
- className="h-4 w-4"
118
- src={attachmentIcon}
119
- />
120
- </div>
96
+ {snapTarget && (
97
+ <SnapTargetBadge className="absolute right-1 bottom-1" target={snapTarget} />
121
98
  )}
122
99
  </div>
123
100
  <span className="truncate px-0.5 text-left font-medium text-[11px] text-muted-foreground group-hover:text-foreground">
@@ -3,7 +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
+ import { getLevelDisplayName } from '@pascal-app/core'
7
7
  import { cn } from '../../lib/utils'
8
8
  import {
9
9
  Dialog,