@pascal-app/editor 0.9.1 → 1.0.0-beta.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 (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -1,44 +1,303 @@
1
1
  'use client'
2
2
 
3
- import { nodeRegistry } from '@pascal-app/core'
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ nodeRegistry,
7
+ type TerrainVerb,
8
+ useScene,
9
+ } from '@pascal-app/core'
10
+ import { useViewer } from '@pascal-app/viewer'
11
+ import { useEffect, useMemo, useState } from 'react'
12
+ import { useShallow } from 'zustand/react/shallow'
4
13
  import { useIsMobile } from '../../../hooks/use-mobile'
5
- import useEditor from '../../../store/use-editor'
14
+ import {
15
+ type ContextualShortcutHint,
16
+ GROUP_MOVE_DRAG_LABEL,
17
+ GROUP_ROTATE_DRAG_LABEL,
18
+ ROTATE_HANDLE_DRAG_LABEL,
19
+ resolveRotateHandleHelpHints,
20
+ resolveSelectModeHelpHints,
21
+ } from '../../../lib/contextual-help'
22
+ import { continuationContextOf } from '../../../lib/continuation'
23
+ import { canDirectMoveNode, canDirectRotateNode } from '../../../lib/direct-manipulation'
24
+ import type { ReshapeKind } from '../../../lib/interaction/scope'
25
+ import { isFreshPlacementMetadata } from '../../../lib/placement-metadata'
26
+ import { snapContextOf } from '../../../lib/snapping-mode'
27
+ import useEditor, { getActiveContinuationContext } from '../../../store/use-editor'
28
+ import useInteractionScope, {
29
+ useActiveHandleDrag,
30
+ useMovingNode,
31
+ } from '../../../store/use-interaction-scope'
6
32
  import { BuildingHelper } from './building-helper'
33
+ import { ContextualHelperPanel } from './contextual-helper-panel'
7
34
  import { ItemHelper } from './item-helper'
8
35
  import { RegisteredToolHelper } from './registered-tool-helper'
9
36
  import { RoofHelper } from './roof-helper'
10
37
 
38
+ // Reshaping a selected node's geometry (endpoint / curve / polygon corner). The
39
+ // snapping chip is the main control; these just name the gesture + Esc.
40
+ function reshapingHints(reshape: ReshapeKind): ContextualShortcutHint[] {
41
+ const action =
42
+ reshape === 'curve'
43
+ ? 'Curve'
44
+ : reshape === 'control-point'
45
+ ? 'Move control point'
46
+ : reshape === 'tangent'
47
+ ? 'Move tangent'
48
+ : reshape === 'endpoint'
49
+ ? 'Move endpoint'
50
+ : 'Move corner'
51
+ return [
52
+ { keys: ['Drag'], label: action },
53
+ { keys: ['Esc'], label: 'Cancel' },
54
+ ]
55
+ }
56
+
57
+ // Sculpt mode's HUD. The verb is named rather than described because it is the one
58
+ // piece of state a user loses track of between strokes. This stays even though the
59
+ // brush ring now carries the verb in colour (`brushRingColor`): the ring says
60
+ // *which* verb only to someone who already knows the mapping, and the HUD is what
61
+ // teaches it.
62
+ function terrainSculptHints(verb: TerrainVerb, sampling: boolean): ContextualShortcutHint[] {
63
+ if (sampling) {
64
+ return [
65
+ { keys: ['Click'], label: 'Pick target height' },
66
+ { keys: ['Esc'], label: 'Cancel picking' },
67
+ ]
68
+ }
69
+ const action =
70
+ verb === 'raise'
71
+ ? 'Raise ground'
72
+ : verb === 'lower'
73
+ ? 'Lower ground'
74
+ : verb === 'flatten'
75
+ ? 'Level ground'
76
+ : 'Smooth ground'
77
+ return [
78
+ { keys: ['Drag'], label: action },
79
+ // Nested, so the two render as alternatives ("[ / ]") rather than a chord —
80
+ // a flat `['[', ']']` joins with "+" and would read as "press both".
81
+ { keys: [['[', ']']], label: 'Brush size' },
82
+ { keys: ['Esc'], label: 'Cancel stroke' },
83
+ ]
84
+ }
85
+
86
+ type ActiveModifierKeys = {
87
+ command: boolean
88
+ shift: boolean
89
+ }
90
+
91
+ function useActiveModifierKeys(): ActiveModifierKeys {
92
+ const [modifiers, setModifiers] = useState<ActiveModifierKeys>({
93
+ command: false,
94
+ shift: false,
95
+ })
96
+
97
+ useEffect(() => {
98
+ const updateModifiers = (event: KeyboardEvent) => {
99
+ const isKeyDown = event.type === 'keydown'
100
+ setModifiers({
101
+ command:
102
+ event.metaKey ||
103
+ event.ctrlKey ||
104
+ (isKeyDown && (event.key === 'Meta' || event.key === 'Control')),
105
+ shift: event.shiftKey || (isKeyDown && event.key === 'Shift'),
106
+ })
107
+ }
108
+ const clearModifiers = () => {
109
+ setModifiers({ command: false, shift: false })
110
+ }
111
+
112
+ window.addEventListener('keydown', updateModifiers)
113
+ window.addEventListener('keyup', updateModifiers)
114
+ window.addEventListener('blur', clearModifiers)
115
+ return () => {
116
+ window.removeEventListener('keydown', updateModifiers)
117
+ window.removeEventListener('keyup', updateModifiers)
118
+ window.removeEventListener('blur', clearModifiers)
119
+ }
120
+ }, [])
121
+
122
+ return modifiers
123
+ }
124
+
11
125
  export function HelperManager() {
12
126
  const mode = useEditor((s) => s.mode)
13
127
  const tool = useEditor((s) => s.tool)
14
- const movingNode = useEditor((state) => state.movingNode)
128
+ const terrainVerb = useEditor((s) => s.terrainVerb)
129
+ const terrainSampling = useEditor((s) => s.terrainSampling)
130
+ const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
131
+ const measurementToolKind = useEditor((s) => s.toolDefaults.measurement?.kind)
132
+ const workspaceMode = useEditor((s) => s.workspaceMode)
133
+ const scope = useInteractionScope((s) => s.scope)
134
+ const movingNode = useMovingNode()
135
+ const activeHandleDrag = useActiveHandleDrag()
136
+ const selectedIds = useViewer((s) => s.selection.selectedIds)
15
137
  const isMobile = useIsMobile()
138
+ const modifiers = useActiveModifierKeys()
139
+ const selectedNodes = useScene(
140
+ useShallow((s) =>
141
+ selectedIds
142
+ .map((id) => s.nodes[id as AnyNodeId])
143
+ .filter((node): node is AnyNode => node !== undefined),
144
+ ),
145
+ )
146
+ // The snapping context for whatever's active (wall / item / polygon) — drives
147
+ // which snapping chips the HUD shows, derived once and shared by every branch.
148
+ const snapContext = useMemo(
149
+ () =>
150
+ snapContextOf({
151
+ scope,
152
+ mode,
153
+ tool,
154
+ profileOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapProfile,
155
+ draftDirectionalOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapDraftDirectional ?? true,
156
+ }),
157
+ [scope, mode, tool],
158
+ )
159
+ const continuationContext = useMemo(
160
+ () => getActiveContinuationContext(),
161
+ [scope, mode, tool],
162
+ )
163
+ const selectModeHints = useMemo(() => {
164
+ const single = selectedNodes.length === 1 ? selectedNodes[0] : null
165
+ const mepSelection =
166
+ single?.type === 'duct-segment' || single?.type === 'pipe-segment'
167
+ ? 'run'
168
+ : single?.type === 'duct-fitting' || single?.type === 'pipe-fitting'
169
+ ? 'fitting'
170
+ : null
171
+ return resolveSelectModeHelpHints({
172
+ selectedCount: selectedNodes.length,
173
+ hasMovableSelection: selectedNodes.some((node) => canDirectMoveNode(node)),
174
+ hasRotatableSelection: selectedNodes.some((node) => canDirectRotateNode(node)),
175
+ commandPressed: modifiers.command,
176
+ shiftPressed: modifiers.shift,
177
+ mepSelection,
178
+ })
179
+ }, [modifiers.command, modifiers.shift, selectedNodes])
16
180
 
17
181
  // Helpers are keyboard-driven hints (Esc, R, etc.) — irrelevant on touch.
18
182
  if (isMobile) return null
19
183
 
184
+ // First-person walkthrough has its own HUD; editor shortcut hints (e.g. the
185
+ // Ctrl multi-select hint — Ctrl is crouch there) don't apply while walking.
186
+ if (isFirstPersonMode) return null
187
+
188
+ // The studio workspace (compose panel / gallery) has no scene selection or
189
+ // tools — editor shortcut hints would only mislead there.
190
+ if (workspaceMode === 'studio') return null
191
+
192
+ // Rotating a node (or a multi-selection group) via its in-world gizmo:
193
+ // advertise Shift = free rotation, the same angle-step bypass wall drafting
194
+ // exposes. Takes priority over the idle select-mode hints since a handle
195
+ // drag is the active interaction.
196
+ if (
197
+ activeHandleDrag?.label === ROTATE_HANDLE_DRAG_LABEL ||
198
+ activeHandleDrag?.label === GROUP_ROTATE_DRAG_LABEL
199
+ ) {
200
+ return <ContextualHelperPanel hints={resolveRotateHandleHelpHints(modifiers.shift)} />
201
+ }
202
+
203
+ // Group-move drag / pick-up: the drag resolves to the 'item' snap context
204
+ // (see `snapContextOf`), so surface the snapping chips — mode + grid step,
205
+ // with their Shift / Ctrl cycle shortcuts — plus the mid-move R/T rotate.
206
+ if (activeHandleDrag?.label === GROUP_MOVE_DRAG_LABEL) {
207
+ return (
208
+ <ContextualHelperPanel
209
+ hints={[{ keys: ['R / T'], label: 'Rotate the selection ±45°' }]}
210
+ snapContext={snapContext}
211
+ />
212
+ )
213
+ }
214
+
215
+ // Reshaping a node's geometry (endpoint / curve / polygon corner). Checked
216
+ // before the select branch so the idle "drag selected / add objects" hints
217
+ // never leak over an in-progress reshape — and it gets its own snapping chip.
218
+ if (scope.kind === 'reshaping') {
219
+ return <ContextualHelperPanel hints={reshapingHints(scope.reshape)} snapContext={snapContext} />
220
+ }
221
+
20
222
  if (movingNode) {
21
223
  if (movingNode.type === 'building') return <BuildingHelper showRotate />
22
- return <ItemHelper showEsc />
224
+ // A fresh placement (e.g. a positioned preset like a shelf) advertises its
225
+ // once/repeat continuation, exactly like the GLB item tool — but an existing
226
+ // node being *moved* is not a placement, so it gets no continuation chip.
227
+ const movingContinuationContext = isFreshPlacementMetadata(movingNode.metadata)
228
+ ? continuationContextOf(movingNode.type)
229
+ : null
230
+ // Force-place only makes sense for kinds that collision-validate their drop;
231
+ // structural kinds (wall/slab/…) never reject, so don't advertise Alt.
232
+ return (
233
+ <ItemHelper
234
+ continuationContext={movingContinuationContext}
235
+ showEsc
236
+ showForce={nodeRegistry.get(movingNode.type)?.snapProfile !== 'structural'}
237
+ snapContext={snapContext}
238
+ />
239
+ )
23
240
  }
24
241
 
242
+ // Paint mode advertises (and cycles, via Shift) the application scope — the
243
+ // only contextual control here. The chip hides itself for targets that only
244
+ // paint one surface, so this renders nothing until a scoped target is active.
25
245
  if (mode === 'material-paint') {
26
- return null
246
+ return <ContextualHelperPanel hints={[]} showPaintScope />
247
+ }
248
+
249
+ // Sculpt mode. The HUD is what makes a sustained brush mode legible: it names
250
+ // the active verb (the same word the panel's segmented control shows, so the
251
+ // two never disagree), and it advertises the two keys whose behaviour is
252
+ // mode-specific — bracket resize, and an Esc that abandons the stroke rather
253
+ // than exiting the mode.
254
+ if (mode === 'terrain-sculpt') {
255
+ return <ContextualHelperPanel hints={terrainSculptHints(terrainVerb, terrainSampling)} />
256
+ }
257
+
258
+ // Idle select only — an active scope (handle-drag, box-select, …) must not show
259
+ // the idle selection hints.
260
+ if (mode === 'select' && scope.kind === 'idle') {
261
+ return <ContextualHelperPanel hints={selectModeHints} />
262
+ }
263
+
264
+ if (tool === 'measurement' && measurementToolKind === 'smart') {
265
+ return (
266
+ <ContextualHelperPanel
267
+ hints={[
268
+ { keys: ['Hover'], label: 'Inspect surface dimensions' },
269
+ { keys: ['Click'], label: 'Pin measurement lens' },
270
+ { keys: ['Esc'], label: 'Exit smart measure' },
271
+ ]}
272
+ />
273
+ )
27
274
  }
28
275
 
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.
276
+ // Legacy fallback only `roof` remains because it hasn't migrated to
277
+ // `def.tool` / `def.toolHints` yet (no Stage D port). Checked before the
278
+ // generic tool branch so the snap-context fallback below doesn't capture it
279
+ // and drop its bespoke `RoofHelper` hints. When roof migrates, this deletes.
280
+ if (tool === 'roof') return <RoofHelper snapContext={snapContext} />
281
+
282
+ // Registry-first: a kind renders the generic `RegisteredToolHelper` when it
283
+ // declares `def.toolHints`, OR whenever its draft resolves to a snap /
284
+ // continuation context — so a snappable tool with NO hand-written hints (e.g.
285
+ // `zone`) still advertises the snapping chip it already honors (Shift = cycle).
286
+ // `RegisteredToolHelper` self-hides when there's genuinely nothing to show.
32
287
  if (tool) {
33
288
  const def = nodeRegistry.get(tool)
34
- if (def?.toolHints && def.toolHints.length > 0) {
35
- return <RegisteredToolHelper hints={def.toolHints} />
289
+ const hints = def?.toolHints ?? []
290
+ if (hints.length > 0 || snapContext || continuationContext) {
291
+ return (
292
+ <RegisteredToolHelper
293
+ continuationContext={continuationContext}
294
+ hints={hints}
295
+ shiftPressed={modifiers.shift}
296
+ snapContext={snapContext}
297
+ />
298
+ )
36
299
  }
37
300
  }
38
301
 
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
302
  return null
44
303
  }
@@ -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',