@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
@@ -9,17 +9,58 @@ import {
9
9
  TooltipContent,
10
10
  TooltipTrigger,
11
11
  } from './../../../components/ui/primitives/tooltip'
12
+ import { triggerSFX } from './../../../lib/sfx-bus'
12
13
  import { cn } from './../../../lib/utils'
13
14
  import useEditor, { type CatalogCategory } from './../../../store/use-editor'
14
15
  import { CATALOG_ITEMS } from './catalog-items'
15
16
 
16
- export function ItemCatalog({ category }: { category: CatalogCategory }) {
17
+ export function ItemCatalog({
18
+ category,
19
+ items: itemsOverride,
20
+ activePlacementTag = null,
21
+ activeFunctionalTag = null,
22
+ search = '',
23
+ overrideItems,
24
+ leadingTile,
25
+ emptyState,
26
+ }: {
27
+ category: CatalogCategory
28
+ items?: AssetInput[]
29
+ activePlacementTag?: string | null
30
+ activeFunctionalTag?: string | null
31
+ search?: string
32
+ /** When set, bypasses all filtering and displays these items directly (used for server search results) */
33
+ overrideItems?: AssetInput[]
34
+ /** Rendered as the first grid cell, always visible when there are items. */
35
+ leadingTile?: React.ReactNode
36
+ /** Rendered when there are no items to show. Replaces the empty grid. */
37
+ emptyState?: React.ReactNode
38
+ }) {
17
39
  const selectedItem = useEditor((state) => state.selectedItem)
18
40
  const setSelectedItem = useEditor((state) => state.setSelectedItem)
41
+ const setMode = useEditor((state) => state.setMode)
42
+ const setTool = useEditor((state) => state.setTool)
19
43
 
20
- const categoryItems = CATALOG_ITEMS.filter((item) => item.category === category)
44
+ const sourceItems = itemsOverride ?? CATALOG_ITEMS
45
+ // Server-provided results bypass all local filtering; otherwise filter by category/search/tags
46
+ const filteredItems =
47
+ overrideItems ??
48
+ (() => {
49
+ const categoryItems = search
50
+ ? sourceItems
51
+ : sourceItems.filter((item) => item.category === category)
52
+ return categoryItems.filter((item) => {
53
+ const tags = item.tags ?? []
54
+ if (activePlacementTag && !tags.includes(activePlacementTag)) return false
55
+ if (activeFunctionalTag && !tags.includes(activeFunctionalTag)) return false
56
+ if (search && !item.name.toLowerCase().includes(search.toLowerCase())) return false
57
+ return true
58
+ })
59
+ })()
21
60
 
22
- // Auto-select first item if current selection is not in this category
61
+ const categoryItems = filteredItems
62
+
63
+ // Auto-select first item if current selection is not in the filtered list
23
64
  useEffect(() => {
24
65
  const isCurrentItemInCategory = categoryItems.some((item) => item.src === selectedItem?.src)
25
66
  if (!isCurrentItemInCategory && categoryItems.length > 0) {
@@ -27,58 +68,62 @@ export function ItemCatalog({ category }: { category: CatalogCategory }) {
27
68
  }
28
69
  }, [categoryItems, selectedItem?.src, setSelectedItem])
29
70
 
30
- // Get attachment icon based on attachTo type
31
71
  const getAttachmentIcon = (attachTo: AssetInput['attachTo']) => {
32
- if (attachTo === 'wall' || attachTo === 'wall-side') {
33
- return '/icons/wall.png'
34
- }
35
- if (attachTo === 'ceiling') {
36
- return '/icons/ceiling.png'
37
- }
72
+ if (attachTo === 'wall' || attachTo === 'wall-side') return '/icons/wall.png'
73
+ if (attachTo === 'ceiling') return '/icons/ceiling.png'
38
74
  return null
39
75
  }
40
76
 
77
+ if (filteredItems.length === 0 && emptyState) {
78
+ return <>{emptyState}</>
79
+ }
80
+
41
81
  return (
42
- <div className="-mx-2 -my-2 flex max-w-xl gap-2 overflow-x-auto p-2">
43
- {categoryItems.map((item, index) => {
82
+ <div
83
+ className="grid gap-2"
84
+ style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(90px, 1fr))' }}
85
+ >
86
+ {leadingTile}
87
+ {filteredItems.map((item, index) => {
44
88
  const isSelected = selectedItem?.src === item?.src
45
89
  const attachmentIcon = getAttachmentIcon(item?.attachTo)
46
90
  return (
47
- <Tooltip key={index}>
48
- <TooltipTrigger asChild>
49
- <button
50
- className={cn(
51
- 'relative aspect-square h-14 min-h-14 w-14 min-w-14 shrink-0 flex-col gap-px rounded-lg transition-all duration-200 ease-out hover:scale-105 hover:cursor-pointer',
52
- isSelected && 'ring-2 ring-primary-foreground',
53
- )}
54
- onClick={() => setSelectedItem(item)}
55
- type="button"
56
- >
57
- <Image
58
- alt={item.name}
59
- className="rounded-lg object-cover"
60
- fill
61
- loading="eager"
62
- sizes="56px"
63
- src={resolveCdnUrl(item.thumbnail) || ''}
64
- />
65
- {attachmentIcon && (
66
- <div className="absolute right-0.5 bottom-0.5 flex h-4 w-4 items-center justify-center rounded bg-black/60">
67
- <Image
68
- alt={item.attachTo === 'ceiling' ? 'Ceiling attachment' : 'Wall attachment'}
69
- className="h-4 w-4"
70
- height={16}
71
- src={attachmentIcon}
72
- width={16}
73
- />
74
- </div>
75
- )}
76
- </button>
77
- </TooltipTrigger>
78
- <TooltipContent className="text-xs" side="top">
91
+ <button
92
+ className={cn(
93
+ 'group relative flex flex-col gap-1.5 rounded-xl p-1.5 transition-colors hover:cursor-pointer hover:bg-sidebar-accent',
94
+ isSelected && 'bg-sidebar-accent ring-2 ring-primary-foreground',
95
+ )}
96
+ key={index}
97
+ onClick={() => {
98
+ triggerSFX('sfx:menu-click')
99
+ setSelectedItem(item)
100
+ setTool('item')
101
+ setMode('build')
102
+ }}
103
+ onMouseEnter={() => triggerSFX('sfx:menu-hover')}
104
+ type="button"
105
+ >
106
+ <div className="relative aspect-square w-full overflow-hidden rounded-lg">
107
+ <img
108
+ alt={item.name}
109
+ className="h-full w-full object-cover"
110
+ loading="eager"
111
+ src={resolveCdnUrl(item.thumbnail) || ''}
112
+ />
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>
121
+ )}
122
+ </div>
123
+ <span className="truncate px-0.5 text-left font-medium text-[11px] text-muted-foreground group-hover:text-foreground">
79
124
  {item.name}
80
- </TooltipContent>
81
- </Tooltip>
125
+ </span>
126
+ </button>
82
127
  )
83
128
  })}
84
129
  </div>
@@ -2,8 +2,9 @@
2
2
 
3
3
  import type { LevelNode } from '@pascal-app/core'
4
4
  import { useEffect, useState } from 'react'
5
- import { cn } from '../../lib/utils'
6
5
  import type { LevelDuplicatePreset } from '../../lib/level-duplication'
6
+ import { getLevelDisplayName } from '../../lib/level-name'
7
+ import { cn } from '../../lib/utils'
7
8
  import {
8
9
  Dialog,
9
10
  DialogContent,
@@ -42,7 +43,7 @@ const DUPLICATE_PRESETS: Array<{
42
43
 
43
44
  function getLevelLabel(level: LevelNode | null) {
44
45
  if (!level) return 'this level'
45
- return level.name || `Level ${level.level}`
46
+ return getLevelDisplayName(level)
46
47
  }
47
48
 
48
49
  export function LevelDuplicateDialog({
@@ -69,9 +70,7 @@ export function LevelDuplicateDialog({
69
70
  <DialogContent className="sm:max-w-md" showCloseButton={false}>
70
71
  <DialogHeader>
71
72
  <DialogTitle>Duplicate Level</DialogTitle>
72
- <DialogDescription>
73
- Choose what to copy from {getLevelLabel(level)}.
74
- </DialogDescription>
73
+ <DialogDescription>Choose what to copy from {getLevelLabel(level)}.</DialogDescription>
75
74
  </DialogHeader>
76
75
 
77
76
  <div className="grid gap-2">
@@ -95,7 +94,7 @@ export function LevelDuplicateDialog({
95
94
 
96
95
  <DialogFooter>
97
96
  <button
98
- className="cursor-pointer rounded-md px-4 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent"
97
+ className="cursor-pointer rounded-md px-4 py-2 text-muted-foreground text-sm transition-colors hover:bg-accent"
99
98
  onClick={() => onOpenChange(false)}
100
99
  type="button"
101
100
  >
@@ -13,6 +13,7 @@ const TYPE_DEFAULTS: Record<string, NodeDisplay> = {
13
13
  slab: { icon: '/icons/floor.png', label: 'Slab' },
14
14
  ceiling: { icon: '/icons/ceiling.png', label: 'Ceiling' },
15
15
  column: { icon: '/icons/column.png', label: 'Column' },
16
+ elevator: { icon: '/icons/elevator.png', label: 'Elevator' },
16
17
  fence: { icon: '/icons/fence.png', label: 'Fence' },
17
18
  roof: { icon: '/icons/roof.png', label: 'Roof' },
18
19
  'roof-segment': { icon: '/icons/roof.png', label: 'Roof segment' },
@@ -1,9 +1,8 @@
1
1
  'use client'
2
2
 
3
3
  import useEditor from '../../../store/use-editor'
4
- import { SliderControl } from '../controls/slider-control'
5
- import { Input } from '../primitives/input'
6
4
  import { PanelSection } from '../controls/panel-section'
5
+ import { Input } from '../primitives/input'
7
6
  import { PanelWrapper } from './panel-wrapper'
8
7
 
9
8
  function buildDefaultCustomMaterial() {
@@ -53,11 +52,7 @@ export function PaintPanel() {
53
52
  }
54
53
 
55
54
  return (
56
- <PanelWrapper
57
- onClose={() => setPaintPanelOpen(false)}
58
- title="Material"
59
- width={320}
60
- >
55
+ <PanelWrapper onClose={() => setPaintPanelOpen(false)} title="Material" width={320}>
61
56
  <PanelSection title="Custom Material">
62
57
  <div className="space-y-3">
63
58
  <div className="space-y-2">
@@ -78,41 +73,71 @@ export function PaintPanel() {
78
73
  </div>
79
74
  </div>
80
75
 
81
- <div className="space-y-1">
82
- <label className="block font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
83
- Surface
84
- </label>
85
- <div className="space-y-1 rounded-lg border border-border/50 bg-background/40 p-2">
86
- <SliderControl
87
- label="Roughness"
88
- max={1}
89
- min={0}
90
- onChange={(roughness) => updateCustomMaterial({ roughness })}
91
- precision={2}
92
- step={0.01}
93
- value={currentProps.roughness}
94
- />
95
- <SliderControl
96
- label="Metalness"
97
- max={1}
98
- min={0}
99
- onChange={(metalness) => updateCustomMaterial({ metalness })}
100
- precision={2}
101
- step={0.01}
102
- value={currentProps.metalness}
103
- />
104
- <SliderControl
105
- label="Opacity"
106
- max={1}
107
- min={0}
108
- onChange={(opacity) =>
109
- updateCustomMaterial({ opacity }, opacity < 1 || currentProps.transparent)
110
- }
111
- precision={2}
112
- step={0.01}
113
- value={currentProps.opacity}
114
- />
76
+ <div className="space-y-2">
77
+ <div className="flex items-center justify-between">
78
+ <label className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
79
+ Roughness
80
+ </label>
81
+ <span className="font-mono text-muted-foreground text-xs">
82
+ {currentProps.roughness.toFixed(2)}
83
+ </span>
84
+ </div>
85
+ <input
86
+ className="h-2 w-full cursor-pointer appearance-none rounded-full bg-accent"
87
+ max={1}
88
+ min={0}
89
+ onChange={(e) =>
90
+ updateCustomMaterial({ roughness: Number.parseFloat(e.target.value) })
91
+ }
92
+ step={0.01}
93
+ type="range"
94
+ value={currentProps.roughness}
95
+ />
96
+ </div>
97
+
98
+ <div className="space-y-2">
99
+ <div className="flex items-center justify-between">
100
+ <label className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
101
+ Metalness
102
+ </label>
103
+ <span className="font-mono text-muted-foreground text-xs">
104
+ {currentProps.metalness.toFixed(2)}
105
+ </span>
106
+ </div>
107
+ <input
108
+ className="h-2 w-full cursor-pointer appearance-none rounded-full bg-accent"
109
+ max={1}
110
+ min={0}
111
+ onChange={(e) =>
112
+ updateCustomMaterial({ metalness: Number.parseFloat(e.target.value) })
113
+ }
114
+ step={0.01}
115
+ type="range"
116
+ value={currentProps.metalness}
117
+ />
118
+ </div>
119
+
120
+ <div className="space-y-2">
121
+ <div className="flex items-center justify-between">
122
+ <label className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
123
+ Opacity
124
+ </label>
125
+ <span className="font-mono text-muted-foreground text-xs">
126
+ {currentProps.opacity.toFixed(2)}
127
+ </span>
115
128
  </div>
129
+ <input
130
+ className="h-2 w-full cursor-pointer appearance-none rounded-full bg-accent"
131
+ max={1}
132
+ min={0}
133
+ onChange={(e) => {
134
+ const opacity = Number.parseFloat(e.target.value)
135
+ updateCustomMaterial({ opacity }, opacity < 1 || currentProps.transparent)
136
+ }}
137
+ step={0.01}
138
+ type="range"
139
+ value={currentProps.opacity}
140
+ />
116
141
  </div>
117
142
 
118
143
  <div className="space-y-2">
@@ -5,8 +5,11 @@ import {
5
5
  type AnyNodeId,
6
6
  type BuildingNode,
7
7
  type CeilingNode,
8
+ type ChimneyNode,
8
9
  type ColumnNode,
9
10
  type DoorNode,
11
+ type DormerNode,
12
+ type ElevatorNode,
10
13
  type FenceNode,
11
14
  type ItemNode,
12
15
  type RoofNode,
@@ -23,31 +26,22 @@ import { useCallback, useEffect, useState } from 'react'
23
26
  import { useIsMobile } from '../../../hooks/use-mobile'
24
27
  import { sfxEmitter } from '../../../lib/sfx-bus'
25
28
  import useEditor from '../../../store/use-editor'
26
- import { CeilingPanel } from './ceiling-panel'
27
- import { ColumnPanel } from './column-panel'
28
- import { DoorPanel } from './door-panel'
29
- import { FencePanel } from './fence-panel'
30
- import { ItemPanel } from './item-panel'
31
29
  import { MobilePanelSheet } from './mobile-panel-sheet'
32
30
  import { MobileSelectionBar } from './mobile-selection-bar'
33
31
  import { getNodeDisplay } from './node-display'
34
32
  import { PaintPanel } from './paint-panel'
33
+ import { ParametricInspector } from './parametric-inspector'
35
34
  import { ReferencePanel } from './reference-panel'
36
- import { RoofPanel } from './roof-panel'
37
- import { RoofSegmentPanel } from './roof-segment-panel'
38
- import { SlabPanel } from './slab-panel'
39
- import { SpawnPanel } from './spawn-panel'
40
- import { StairPanel } from './stair-panel'
41
- import { StairSegmentPanel } from './stair-segment-panel'
42
- import { WallPanel } from './wall-panel'
43
- import { WindowPanel } from './window-panel'
44
35
 
45
36
  type MovableNode =
46
37
  | ItemNode
47
38
  | WindowNode
48
39
  | DoorNode
40
+ | ElevatorNode
49
41
  | CeilingNode
42
+ | ChimneyNode
50
43
  | ColumnNode
44
+ | DormerNode
51
45
  | SlabNode
52
46
  | WallNode
53
47
  | FenceNode
@@ -61,8 +55,11 @@ const MOVABLE_TYPES = new Set<string>([
61
55
  'item',
62
56
  'window',
63
57
  'door',
58
+ 'elevator',
64
59
  'ceiling',
60
+ 'chimney',
65
61
  'column',
62
+ 'dormer',
66
63
  'slab',
67
64
  'wall',
68
65
  'fence',
@@ -77,36 +74,17 @@ function isMovableNode(node: AnyNode | null): node is MovableNode {
77
74
  return !!node && MOVABLE_TYPES.has(node.type)
78
75
  }
79
76
 
80
- function panelForType(type: string | null) {
77
+ function panelForType(type: string | null, footer?: React.ReactNode) {
81
78
  if (!type) return null
82
- switch (type) {
83
- case 'item':
84
- return <ItemPanel />
85
- case 'roof':
86
- return <RoofPanel />
87
- case 'roof-segment':
88
- return <RoofSegmentPanel />
89
- case 'stair':
90
- return <StairPanel />
91
- case 'stair-segment':
92
- return <StairSegmentPanel />
93
- case 'slab':
94
- return <SlabPanel />
95
- case 'ceiling':
96
- return <CeilingPanel />
97
- case 'column':
98
- return <ColumnPanel />
99
- case 'wall':
100
- return <WallPanel />
101
- case 'fence':
102
- return <FencePanel />
103
- case 'door':
104
- return <DoorPanel />
105
- case 'window':
106
- return <WindowPanel />
107
- default:
108
- return null
109
- }
79
+ // Every kind now renders through `<ParametricInspector>`, which either
80
+ // composes auto-derived editors from `parametrics.groups` or lazy-
81
+ // loads the kind-owned panel via `parametrics.customPanel`. The
82
+ // hardcoded switch is gone — all per-kind panel layout lives in
83
+ // `nodes/src/<kind>/panel.tsx`. The `type` arg is preserved for
84
+ // future cases where we might want a non-registry fallback (e.g.
85
+ // reference scale, paint mode); leave the function shape intact.
86
+ void type
87
+ return <ParametricInspector footer={footer} />
110
88
  }
111
89
 
112
90
  function MobilePanelLayer({
@@ -190,9 +168,11 @@ function MobilePanelLayer({
190
168
  )
191
169
  }
192
170
 
193
- export function PanelManager() {
171
+ export function PanelManager({ inspectorFooter }: { inspectorFooter?: React.ReactNode }) {
194
172
  const isMobile = useIsMobile()
195
173
  const selectedIds = useViewer((s) => s.selection.selectedIds)
174
+ const selectedZoneId = useViewer((s) => s.selection.zoneId)
175
+ const setSelection = useViewer((s) => s.setSelection)
196
176
  const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
197
177
  const isPaintPanelOpen = useEditor((s) => s.isPaintPanelOpen)
198
178
  const mode = useEditor((s) => s.mode)
@@ -237,37 +217,16 @@ export function PanelManager() {
237
217
  return <PaintPanel />
238
218
  }
239
219
 
240
- // Show appropriate panel based on selected node type
241
- if (selectedNodeType) {
242
- switch (selectedNodeType) {
243
- case 'item':
244
- return <ItemPanel />
245
- case 'roof':
246
- return <RoofPanel />
247
- case 'roof-segment':
248
- return <RoofSegmentPanel />
249
- case 'stair':
250
- return <StairPanel />
251
- case 'stair-segment':
252
- return <StairSegmentPanel />
253
- case 'slab':
254
- return <SlabPanel />
255
- case 'spawn':
256
- return <SpawnPanel />
257
- case 'ceiling':
258
- return <CeilingPanel />
259
- case 'column':
260
- return <ColumnPanel />
261
- case 'wall':
262
- return <WallPanel />
263
- case 'fence':
264
- return <FencePanel />
265
- case 'door':
266
- return <DoorPanel />
267
- case 'window':
268
- return <WindowPanel />
269
- }
220
+ if (selectedZoneId && selectedIds.length === 0) {
221
+ return (
222
+ <ParametricInspector
223
+ footer={inspectorFooter}
224
+ key={selectedZoneId}
225
+ nodeId={selectedZoneId as AnyNodeId}
226
+ onClose={() => setSelection({ zoneId: null })}
227
+ />
228
+ )
270
229
  }
271
230
 
272
- return null
231
+ return panelForType(selectedNodeType, inspectorFooter)
273
232
  }