@pascal-app/editor 0.8.0 → 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 (350) hide show
  1. package/package.json +15 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +795 -28
  5. package/src/components/editor/editor-layout-mobile.tsx +8 -3
  6. package/src/components/editor/editor-layout-v2.tsx +76 -51
  7. package/src/components/editor/export-manager.tsx +69 -32
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
  10. package/src/components/editor/first-person/build-collider-world.ts +228 -83
  11. package/src/components/editor/first-person-controls.tsx +943 -27
  12. package/src/components/editor/floating-action-menu.tsx +676 -208
  13. package/src/components/editor/floating-building-action-menu.tsx +2 -2
  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 +6916 -12137
  17. package/src/components/editor/grid.tsx +197 -36
  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 +420 -0
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +396 -0
  24. package/src/components/editor/group-transform-shared.ts +489 -0
  25. package/src/components/editor/handles/handle-arrow.tsx +581 -0
  26. package/src/components/editor/handles/use-handle-drag.ts +246 -0
  27. package/src/components/editor/index.tsx +412 -162
  28. package/src/components/editor/measurement-pill.tsx +115 -0
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +1646 -0
  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 +1106 -483
  36. package/src/components/editor/site-edge-labels.tsx +45 -21
  37. package/src/components/editor/slab-hole-highlights.tsx +474 -0
  38. package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +140 -41
  41. package/src/components/editor/use-floorplan-background-placement.ts +190 -35
  42. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  43. package/src/components/editor/use-floorplan-scene-data.ts +19 -7
  44. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  45. package/src/components/editor/wall-measurement-label.tsx +14 -19
  46. package/src/components/editor/wall-move-side-handles.tsx +870 -0
  47. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  48. package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
  49. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
  50. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  51. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  52. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  53. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  54. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  55. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  56. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  57. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
  58. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
  59. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  60. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
  61. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  62. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  63. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  64. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  65. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  66. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
  67. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  68. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
  69. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  70. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  71. package/src/components/editor-2d/svg-paths.ts +10 -2
  72. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
  73. package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
  74. package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
  75. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  76. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  77. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  78. package/src/components/systems/zone/zone-system.tsx +42 -13
  79. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  80. package/src/components/tools/elevator/elevator-tool.tsx +375 -0
  81. package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
  82. package/src/components/tools/fence/fence-drafting.ts +141 -27
  83. package/src/components/tools/item/move-tool.tsx +45 -105
  84. package/src/components/tools/item/placement-math.test.ts +22 -0
  85. package/src/components/tools/item/placement-math.ts +59 -7
  86. package/src/components/tools/item/placement-strategies.ts +432 -16
  87. package/src/components/tools/item/placement-types.ts +22 -1
  88. package/src/components/tools/item/use-draft-node.ts +51 -2
  89. package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
  90. package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
  91. package/src/components/tools/roof/roof-tool.tsx +350 -28
  92. package/src/components/tools/select/box-select-state.ts +88 -0
  93. package/src/components/tools/select/box-select-tool.tsx +442 -520
  94. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  95. package/src/components/tools/select/marquee-geometry.ts +155 -0
  96. package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
  97. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  98. package/src/components/tools/select/select-candidates.test.ts +109 -0
  99. package/src/components/tools/select/select-candidates.ts +85 -0
  100. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  101. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  102. package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
  103. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  104. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  105. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  106. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  107. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  108. package/src/components/tools/shared/placement-box.tsx +305 -0
  109. package/src/components/tools/shared/polygon-editor.tsx +866 -175
  110. package/src/components/tools/shared/segment-angle.ts +56 -0
  111. package/src/components/tools/site/site-boundary-editor.tsx +520 -16
  112. package/src/components/tools/site/site-flag-model.ts +18 -0
  113. package/src/components/tools/stair/stair-defaults.ts +2 -1
  114. package/src/components/tools/stair/stair-tool.tsx +342 -37
  115. package/src/components/tools/tool-manager.tsx +239 -62
  116. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  117. package/src/components/tools/wall/wall-drafting.ts +248 -171
  118. package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
  119. package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
  120. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  121. package/src/components/tools/zone/zone-tool.tsx +86 -82
  122. package/src/components/ui/action-menu/action-button.tsx +21 -1
  123. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  124. package/src/components/ui/action-menu/control-modes.tsx +40 -170
  125. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  126. package/src/components/ui/action-menu/index.tsx +3 -117
  127. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  128. package/src/components/ui/action-menu/structure-tools.tsx +27 -93
  129. package/src/components/ui/action-menu/view-toggles.tsx +279 -90
  130. package/src/components/ui/command-palette/editor-commands.tsx +20 -15
  131. package/src/components/ui/command-palette/index.tsx +5 -3
  132. package/src/components/ui/controls/material-paint-panel.tsx +152 -0
  133. package/src/components/ui/controls/material-picker.tsx +87 -177
  134. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  135. package/src/components/ui/controls/metric-control.tsx +142 -46
  136. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  137. package/src/components/ui/controls/slider-control.tsx +88 -38
  138. package/src/components/ui/floating-level-selector.tsx +47 -10
  139. package/src/components/ui/helpers/building-helper.tsx +10 -23
  140. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  141. package/src/components/ui/helpers/helper-manager.tsx +235 -23
  142. package/src/components/ui/helpers/item-helper.tsx +28 -32
  143. package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
  144. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  145. package/src/components/ui/icon-ref.tsx +47 -0
  146. package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
  147. package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
  148. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  149. package/src/components/ui/panels/node-display.ts +17 -16
  150. package/src/components/ui/panels/panel-manager.tsx +55 -58
  151. package/src/components/ui/panels/panel-wrapper.tsx +238 -12
  152. package/src/components/ui/panels/parametric-inspector.tsx +482 -0
  153. package/src/components/ui/panels/reference-panel.tsx +54 -19
  154. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  155. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  156. package/src/components/ui/primitives/sidebar.tsx +1 -0
  157. package/src/components/ui/scene-loader.tsx +1 -3
  158. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  159. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  160. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  161. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  162. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  163. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
  164. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
  165. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
  166. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  167. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
  168. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
  169. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  170. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  171. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
  172. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  173. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
  174. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
  175. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
  176. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  177. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  178. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
  179. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
  180. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
  181. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
  182. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
  183. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  184. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  185. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
  186. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  187. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  188. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  189. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  190. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  191. package/src/components/ui/sidebar/tab-bar.tsx +75 -1
  192. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  193. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  194. package/src/components/ui/snap-target-badge.tsx +88 -0
  195. package/src/components/viewer-overlay.tsx +253 -61
  196. package/src/components/viewer-zone-system.tsx +7 -2
  197. package/src/hooks/use-auto-save.test.ts +14 -0
  198. package/src/hooks/use-auto-save.ts +64 -10
  199. package/src/hooks/use-ceiling-events.ts +176 -0
  200. package/src/hooks/use-drag-action.ts +127 -0
  201. package/src/hooks/use-keyboard.ts +493 -95
  202. package/src/hooks/use-selection.ts +64 -0
  203. package/src/index.tsx +451 -6
  204. package/src/lib/active-placement-surface.ts +35 -0
  205. package/src/lib/ceiling-plan-snap.ts +24 -0
  206. package/src/lib/constants.ts +6 -2
  207. package/src/lib/contextual-help.test.ts +112 -0
  208. package/src/lib/contextual-help.ts +144 -0
  209. package/src/lib/continuation.ts +64 -0
  210. package/src/lib/direct-manipulation.test.ts +192 -0
  211. package/src/lib/direct-manipulation.ts +109 -0
  212. package/src/lib/editor-api.ts +50 -0
  213. package/src/lib/elevator-support.ts +96 -0
  214. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  215. package/src/lib/floorplan/apply-alignment.ts +117 -0
  216. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  217. package/src/lib/floorplan/geometry.ts +53 -0
  218. package/src/lib/floorplan/index.ts +10 -0
  219. package/src/lib/floorplan/items.ts +5 -2
  220. package/src/lib/floorplan/plan-coords.ts +21 -0
  221. package/src/lib/floorplan/selection-tool.ts +40 -0
  222. package/src/lib/floorplan/stairs.ts +12 -1
  223. package/src/lib/fresh-planar-placement.test.ts +339 -0
  224. package/src/lib/fresh-planar-placement.ts +130 -0
  225. package/src/lib/glb-export.test.ts +441 -0
  226. package/src/lib/glb-export.ts +874 -0
  227. package/src/lib/history.ts +11 -1
  228. package/src/lib/interaction/hot-set.test.ts +133 -0
  229. package/src/lib/interaction/hot-set.ts +67 -0
  230. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  231. package/src/lib/interaction/overlay-policy.ts +59 -0
  232. package/src/lib/interaction/scope.ts +211 -0
  233. package/src/lib/level-duplication.ts +54 -42
  234. package/src/lib/level-selection.ts +2 -2
  235. package/src/lib/material-paint.ts +227 -48
  236. package/src/lib/measurement-kind.test.ts +30 -0
  237. package/src/lib/measurement-kind.ts +19 -0
  238. package/src/lib/measurement-label.test.ts +47 -0
  239. package/src/lib/measurement-label.ts +68 -0
  240. package/src/lib/measurement-parser.ts +116 -0
  241. package/src/lib/measurements.test.ts +183 -0
  242. package/src/lib/measurements.ts +199 -0
  243. package/src/lib/paint-scope.test.ts +454 -0
  244. package/src/lib/paint-scope.ts +461 -0
  245. package/src/lib/placement-drag-release.ts +23 -0
  246. package/src/lib/placement-metadata.ts +29 -0
  247. package/src/lib/planar-cursor-placement.test.ts +100 -0
  248. package/src/lib/planar-cursor-placement.ts +42 -0
  249. package/src/lib/plugin-panels.test.ts +19 -0
  250. package/src/lib/plugin-panels.ts +91 -0
  251. package/src/lib/quick-measurement.test.ts +77 -0
  252. package/src/lib/quick-measurement.ts +109 -0
  253. package/src/lib/roof-duplication.ts +7 -7
  254. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  255. package/src/lib/roof-wall-hit.ts +164 -0
  256. package/src/lib/scene-clipboard.test.ts +196 -0
  257. package/src/lib/scene-clipboard.ts +332 -0
  258. package/src/lib/scene.ts +40 -8
  259. package/src/lib/selection-routing.test.ts +298 -0
  260. package/src/lib/selection-routing.ts +175 -0
  261. package/src/lib/sfx/index.ts +1 -0
  262. package/src/lib/sfx/movement-tick.test.ts +65 -0
  263. package/src/lib/sfx/movement-tick.ts +18 -0
  264. package/src/lib/sfx-bus.ts +69 -10
  265. package/src/lib/sfx-player.test.ts +124 -0
  266. package/src/lib/sfx-player.ts +148 -55
  267. package/src/lib/site-boundary.ts +1 -0
  268. package/src/lib/slab-plan-snap.test.ts +93 -0
  269. package/src/lib/slab-plan-snap.ts +108 -0
  270. package/src/lib/snapping-mode.test.ts +138 -0
  271. package/src/lib/snapping-mode.ts +179 -0
  272. package/src/lib/stair-duplication.ts +4 -4
  273. package/src/lib/stair-levels.test.ts +146 -0
  274. package/src/lib/stair-levels.ts +177 -0
  275. package/src/lib/surface-plan-snap.test.ts +71 -0
  276. package/src/lib/surface-plan-snap.ts +256 -0
  277. package/src/lib/use-linear-display.ts +40 -0
  278. package/src/lib/world-grid-snap.ts +231 -0
  279. package/src/lib/zone-content.ts +130 -0
  280. package/src/store/use-alignment-guides.ts +21 -0
  281. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  282. package/src/store/use-editor.tsx +767 -114
  283. package/src/store/use-facing-pose.ts +44 -0
  284. package/src/store/use-fence-curve-draft.ts +21 -0
  285. package/src/store/use-floorplan-draft-preview.ts +101 -0
  286. package/src/store/use-floorplan-marquee.ts +50 -0
  287. package/src/store/use-interaction-scope.test.ts +186 -0
  288. package/src/store/use-interaction-scope.ts +132 -0
  289. package/src/store/use-measurement-draft.test.ts +530 -0
  290. package/src/store/use-measurement-draft.ts +543 -0
  291. package/src/store/use-opening-guides.ts +41 -0
  292. package/src/store/use-placement-preview.ts +39 -0
  293. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  294. package/src/store/use-quick-measurement-hud.ts +77 -0
  295. package/src/store/use-segment-draft-chain.ts +28 -0
  296. package/src/store/use-stair-build-preview.ts +43 -0
  297. package/src/store/use-wall-move-ghosts.ts +36 -0
  298. package/src/store/use-wall-snap-indicator.ts +34 -0
  299. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  300. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  301. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  302. package/src/components/tools/building/move-building-tool.tsx +0 -157
  303. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  304. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  305. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  306. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  307. package/src/components/tools/column/column-tool.tsx +0 -97
  308. package/src/components/tools/column/move-column-tool.tsx +0 -105
  309. package/src/components/tools/door/door-math.ts +0 -110
  310. package/src/components/tools/door/door-tool.tsx +0 -324
  311. package/src/components/tools/door/move-door-tool.tsx +0 -412
  312. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  313. package/src/components/tools/fence/fence-tool.tsx +0 -346
  314. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  315. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  316. package/src/components/tools/item/item-tool.tsx +0 -26
  317. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  318. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  319. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  320. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  321. package/src/components/tools/slab/slab-tool.tsx +0 -322
  322. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  323. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  324. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  325. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  326. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  327. package/src/components/tools/wall/wall-tool.tsx +0 -332
  328. package/src/components/tools/window/move-window-tool.tsx +0 -447
  329. package/src/components/tools/window/window-math.ts +0 -117
  330. package/src/components/tools/window/window-tool.tsx +0 -332
  331. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  332. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  333. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  334. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  335. package/src/components/ui/panels/column-panel.tsx +0 -759
  336. package/src/components/ui/panels/door-panel.tsx +0 -1299
  337. package/src/components/ui/panels/fence-panel.tsx +0 -222
  338. package/src/components/ui/panels/item-panel.tsx +0 -306
  339. package/src/components/ui/panels/paint-panel.tsx +0 -163
  340. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  341. package/src/components/ui/panels/roof-panel.tsx +0 -280
  342. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  343. package/src/components/ui/panels/slab-panel.tsx +0 -250
  344. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  345. package/src/components/ui/panels/stair-panel.tsx +0 -577
  346. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  347. package/src/components/ui/panels/wall-panel.tsx +0 -273
  348. package/src/components/ui/panels/window-panel.tsx +0 -968
  349. package/src/contexts/presets-context.tsx +0 -121
  350. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -1,45 +1,257 @@
1
1
  'use client'
2
2
 
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'
3
12
  import { useIsMobile } from '../../../hooks/use-mobile'
4
- 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'
5
31
  import { BuildingHelper } from './building-helper'
6
- import { CeilingHelper } from './ceiling-helper'
32
+ import { ContextualHelperPanel } from './contextual-helper-panel'
7
33
  import { ItemHelper } from './item-helper'
34
+ import { RegisteredToolHelper } from './registered-tool-helper'
8
35
  import { RoofHelper } from './roof-helper'
9
- import { SlabHelper } from './slab-helper'
10
- import { WallHelper } from './wall-helper'
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
+ }
11
94
 
12
95
  export function HelperManager() {
13
96
  const mode = useEditor((s) => s.mode)
14
97
  const tool = useEditor((s) => s.tool)
15
- 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)
16
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])
17
147
 
18
148
  // Helpers are keyboard-driven hints (Esc, R, etc.) — irrelevant on touch.
19
149
  if (isMobile) return null
20
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
+
21
185
  if (movingNode) {
22
186
  if (movingNode.type === 'building') return <BuildingHelper showRotate />
23
- 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
+ )
24
203
  }
25
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.
26
208
  if (mode === 'material-paint') {
27
- return null
28
- }
29
-
30
- // Show appropriate helper based on current tool
31
- switch (tool) {
32
- case 'wall':
33
- return <WallHelper />
34
- case 'item':
35
- return <ItemHelper />
36
- case 'slab':
37
- return <SlabHelper />
38
- case 'ceiling':
39
- return <CeilingHelper />
40
- case 'roof':
41
- return <RoofHelper />
42
- default:
43
- return null
209
+ return <ContextualHelperPanel hints={[]} showPaintScope />
210
+ }
211
+
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} />
44
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.
241
+ if (tool) {
242
+ const def = nodeRegistry.get(tool)
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
+ )
253
+ }
254
+ }
255
+
256
+ return null
45
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
  }
@@ -0,0 +1,59 @@
1
+ import type { ToolHint } from '@pascal-app/core'
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'
6
+
7
+ /**
8
+ * Generic helper panel rendered from `def.toolHints` data. Matches the
9
+ * visual styling of the hand-written `<WallHelper>` / `<ItemHelper>` /
10
+ * etc. so registry-driven kinds get a consistent look without each kind
11
+ * writing its own component.
12
+ *
13
+ * Drops the need for per-kind helper files entirely — kinds declare
14
+ * their hints as static data in their `NodeDefinition`.
15
+ */
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)
42
+ return (
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
+ />
58
+ )
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',
@@ -628,6 +666,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
628
666
  rotation: [0, 0, 0],
629
667
  scale: [1, 1, 1],
630
668
  attachTo: 'ceiling',
669
+ recessed: true,
631
670
  interactive: {
632
671
  controls: [
633
672
  { kind: 'toggle' },
@@ -1,17 +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'
6
+ import { triggerSFX } from './../../../lib/sfx-bus'
12
7
  import { cn } from './../../../lib/utils'
13
8
  import useEditor, { type CatalogCategory } from './../../../store/use-editor'
14
- import { CATALOG_ITEMS } from './catalog-items'
9
+ import { resolveAssetSnapTarget, SnapTargetBadge } from '../snap-target-badge'
10
+ import { CATALOG_ITEMS, type CatalogItem } from './catalog-items'
15
11
 
16
12
  export function ItemCatalog({
17
13
  category,
@@ -40,9 +36,9 @@ export function ItemCatalog({
40
36
  const setMode = useEditor((state) => state.setMode)
41
37
  const setTool = useEditor((state) => state.setTool)
42
38
 
43
- const sourceItems = itemsOverride ?? CATALOG_ITEMS
39
+ const sourceItems: CatalogItem[] = itemsOverride ?? CATALOG_ITEMS
44
40
  // Server-provided results bypass all local filtering; otherwise filter by category/search/tags
45
- const filteredItems =
41
+ const filteredItems: CatalogItem[] =
46
42
  overrideItems ??
47
43
  (() => {
48
44
  const categoryItems = search
@@ -57,22 +53,6 @@ export function ItemCatalog({
57
53
  })
58
54
  })()
59
55
 
60
- const categoryItems = filteredItems
61
-
62
- // Auto-select first item if current selection is not in the filtered list
63
- useEffect(() => {
64
- const isCurrentItemInCategory = categoryItems.some((item) => item.src === selectedItem?.src)
65
- if (!isCurrentItemInCategory && categoryItems.length > 0) {
66
- setSelectedItem(categoryItems[0] as AssetInput)
67
- }
68
- }, [categoryItems, selectedItem?.src, setSelectedItem])
69
-
70
- const getAttachmentIcon = (attachTo: AssetInput['attachTo']) => {
71
- if (attachTo === 'wall' || attachTo === 'wall-side') return '/icons/wall.png'
72
- if (attachTo === 'ceiling') return '/icons/ceiling.png'
73
- return null
74
- }
75
-
76
56
  if (filteredItems.length === 0 && emptyState) {
77
57
  return <>{emptyState}</>
78
58
  }
@@ -85,7 +65,7 @@ export function ItemCatalog({
85
65
  {leadingTile}
86
66
  {filteredItems.map((item, index) => {
87
67
  const isSelected = selectedItem?.src === item?.src
88
- const attachmentIcon = getAttachmentIcon(item?.attachTo)
68
+ const snapTarget = resolveAssetSnapTarget(item?.attachTo)
89
69
  return (
90
70
  <button
91
71
  className={cn(
@@ -94,10 +74,16 @@ export function ItemCatalog({
94
74
  )}
95
75
  key={index}
96
76
  onClick={() => {
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 })
97
82
  setSelectedItem(item)
98
- setTool('item')
83
+ setTool(item.tool ?? 'item')
99
84
  setMode('build')
100
85
  }}
86
+ onMouseEnter={() => triggerSFX('sfx:menu-hover')}
101
87
  type="button"
102
88
  >
103
89
  <div className="relative aspect-square w-full overflow-hidden rounded-lg">
@@ -107,14 +93,8 @@ export function ItemCatalog({
107
93
  loading="eager"
108
94
  src={resolveCdnUrl(item.thumbnail) || ''}
109
95
  />
110
- {attachmentIcon && (
111
- <div className="absolute right-1 bottom-1 flex h-4 w-4 items-center justify-center rounded bg-black/60">
112
- <img
113
- alt={item.attachTo === 'ceiling' ? 'Ceiling attachment' : 'Wall attachment'}
114
- className="h-4 w-4"
115
- src={attachmentIcon}
116
- />
117
- </div>
96
+ {snapTarget && (
97
+ <SnapTargetBadge className="absolute right-1 bottom-1" target={snapTarget} />
118
98
  )}
119
99
  </div>
120
100
  <span className="truncate px-0.5 text-left font-medium text-[11px] text-muted-foreground group-hover:text-foreground">
@@ -3,6 +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 '@pascal-app/core'
6
7
  import { cn } from '../../lib/utils'
7
8
  import {
8
9
  Dialog,
@@ -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({