@pascal-app/editor 1.0.1 → 1.0.4

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 (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -50,12 +50,13 @@ import { pasteSelectionAndPickUp } from '../editor/group-actions'
50
50
  import {
51
51
  buildLevelDuplicateCreateOps,
52
52
  type LevelDuplicatePreset,
53
- } from '../../lib/level-duplication'
53
+ } from '@pascal-app/core/building'
54
54
  import { getDefaultLevelName, getLevelDisplayName } from '@pascal-app/core'
55
55
  import { deleteLevelWithFallbackSelection } from '../../lib/level-selection'
56
56
  import { unitMemberLevels, leaveUnitFocus } from '../../lib/units'
57
57
  import { useLinearDisplay } from '../../lib/use-linear-display'
58
58
  import { cn } from '../../lib/utils'
59
+ import { useIsMobile } from '../../hooks/use-mobile'
59
60
  import { ActionButton } from './controls/action-button'
60
61
  import { SliderControl } from './controls/slider-control'
61
62
  import { LevelDuplicateDialog } from './level-duplicate-dialog'
@@ -448,11 +449,13 @@ function UnitFocusChip() {
448
449
  // ── Main component ──────────────────────────────────────────────────────────
449
450
 
450
451
  export function FloatingLevelSelector() {
452
+ // Desktop's view bar is a row above the stage; mobile's floats over it.
453
+ const isMobile = useIsMobile()
451
454
  const selectedBuildingId = useViewer((s) => s.selection.buildingId)
452
455
  const levelId = useViewer((s) => s.selection.levelId)
453
456
  const setSelection = useViewer((s) => s.setSelection)
454
457
  const createNode = useScene((s) => s.createNode)
455
- const createNodes = useScene((s) => s.createNodes)
458
+ const applyNodeChanges = useScene((s) => s.applyNodeChanges)
456
459
  const updateNodes = useScene((s) => s.updateNodes)
457
460
 
458
461
  const [deletingLevel, setDeletingLevel] = useState<LevelNode | null>(null)
@@ -560,29 +563,21 @@ export function FloatingLevelSelector() {
560
563
 
561
564
  const handleDuplicateLevel = useCallback(
562
565
  (level: LevelNode, preset: LevelDuplicatePreset = 'everything') => {
563
- const { createOps, newLevelId, shiftedLevels } = buildLevelDuplicateCreateOps({
566
+ const { createOps, newLevelId, updateOps } = buildLevelDuplicateCreateOps({
564
567
  nodes: useScene.getState().nodes,
565
568
  level,
566
569
  levels,
567
570
  preset,
568
571
  })
569
572
 
570
- if (shiftedLevels.length > 0) {
571
- updateNodes(
572
- shiftedLevels.map((shiftedLevel) => ({
573
- id: shiftedLevel.id as AnyNodeId,
574
- data: { level: shiftedLevel.level } as Partial<AnyNode>,
575
- })),
576
- )
577
- }
578
- createNodes(createOps)
573
+ applyNodeChanges({ create: createOps, update: updateOps })
579
574
 
580
575
  setSelection({
581
576
  buildingId: resolvedBuildingId ?? undefined,
582
577
  levelId: newLevelId as LevelNode['id'],
583
578
  })
584
579
  },
585
- [createNodes, levels, resolvedBuildingId, setSelection, updateNodes],
580
+ [applyNodeChanges, levels, resolvedBuildingId, setSelection],
586
581
  )
587
582
 
588
583
  const handlePasteToLevel = useCallback((level: LevelNode) => {
@@ -641,7 +636,7 @@ export function FloatingLevelSelector() {
641
636
 
642
637
  return (
643
638
  <>
644
- <div className="pointer-events-auto absolute top-14 left-3 z-20">
639
+ <div className={cn('pointer-events-auto absolute left-3 z-20', isMobile ? 'top-14' : 'top-3')}>
645
640
  <div className="relative">
646
641
  {/* Floating + at top edge */}
647
642
  {!draggingLevelId && (
@@ -1,14 +1,17 @@
1
+ import type { HudTitle } from '../../../lib/hud-title'
1
2
  import { ContextualHelperPanel } from './contextual-helper-panel'
2
3
 
3
4
  interface BuildingHelperProps {
4
5
  showRotate?: boolean
6
+ title?: HudTitle | null
5
7
  }
6
8
 
7
9
  // Rotate is one hint with both keys (R / T) — never two separate
8
10
  // counterclockwise / clockwise rows — to match every other placement helper.
9
- export function BuildingHelper({ showRotate }: BuildingHelperProps) {
11
+ export function BuildingHelper({ showRotate, title = null }: BuildingHelperProps) {
10
12
  return (
11
13
  <ContextualHelperPanel
14
+ title={title}
12
15
  hints={[
13
16
  { keys: ['Left click'], label: 'Place building' },
14
17
  ...(showRotate ? [{ keys: ['R', 'T'], label: 'Rotate' }] : []),
@@ -1,12 +1,14 @@
1
1
  import { Icon } from '@iconify/react'
2
2
  import type { ToolHint } from '@pascal-app/core'
3
- import { Fragment, useSyncExternalStore } from 'react'
3
+ import { Fragment, useEffect, useSyncExternalStore } from 'react'
4
4
  import {
5
5
  CONTINUATION_PROFILES,
6
6
  type ContinuationContext,
7
7
  } from '../../../lib/continuation'
8
8
  import type { ContextualShortcutHint } from '../../../lib/contextual-help'
9
+ import type { HudTitle } from '../../../lib/hud-title'
9
10
  import { hasActivePaintMaterial } from '../../../lib/material-paint'
11
+ import { usePaintRegionMode } from '../../../lib/paint-region-mode'
10
12
  import { paintScopeLabel, type PaintScope } from '../../../lib/paint-scope'
11
13
  import { sfxEmitter } from '../../../lib/sfx-bus'
12
14
  import {
@@ -17,19 +19,23 @@ import {
17
19
  import { cn } from '../../../lib/utils'
18
20
  import useEditor, { type GridSnapStep } from '../../../store/use-editor'
19
21
  import useFenceCurveDraft from '../../../store/use-fence-curve-draft'
22
+ import { IconRefGlyph } from '../icon-ref'
20
23
  import { ShortcutToken } from '../primitives/shortcut-token'
21
24
  import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip'
25
+ import { useInRightStack } from '../right-stack'
22
26
 
23
27
  // One muted container holds every row — passive key hints and interactive chips
24
- // alike — so the HUD reads as a single panel, not a stack of floating pills. The
25
- // background is near-opaque (`bg-background/95`) with a single backdrop blur so
26
- // active rows stay readable over the 3D scene even while a modifier is held.
28
+ // alike — so the HUD reads as a single panel, not a stack of floating pills. It
29
+ // opens with the tool in hand (icon, name, arming key), then the gesture rows,
30
+ // then — past a hairline — the mode chips (snapping, continuation, …) and Esc.
27
31
  // A 2-track grid: column 1 sizes to `max-content` (the widest key across ALL
28
32
  // rows), column 2 (`1fr`) is the label. Every row is a subgrid sharing those
29
33
  // tracks, so labels align even when keys differ in width (⌘ vs Shift) or wrap to
30
34
  // two lines. Near-opaque bg + single backdrop blur keeps active rows readable.
31
- const CONTAINER_CLASS =
32
- 'pointer-events-none fixed top-1/2 right-4 z-40 grid max-w-[260px] -translate-y-1/2 grid-cols-[max-content_1fr] gap-x-2.5 gap-y-1.5 rounded-lg border border-border bg-background/95 px-3 py-2.5 shadow-lg backdrop-blur-md'
35
+ const CARD_CLASS =
36
+ 'pointer-events-none grid w-[252px] grid-cols-[max-content_1fr] gap-x-2.5 gap-y-1.5 rounded-xl border border-border bg-background/95 p-3 shadow-lg backdrop-blur-md'
37
+ // On its own (no shared right column) it floats centred on the right edge.
38
+ const FLOATING_CLASS = 'fixed top-1/2 right-4 z-40 -translate-y-1/2'
33
39
 
34
40
  const TOKEN_CLASS = 'h-5 px-1.5 text-[10px]'
35
41
 
@@ -260,47 +266,52 @@ function FenceContinuationChips() {
260
266
  const curveStarted = useFenceCurveDraft((s) => s.pointCount > 0)
261
267
 
262
268
  const isCurved = mode === 'curved'
269
+ const isFreehand = mode === 'freehand'
263
270
  const straightMode = isCurved ? 'continuous' : mode
264
- const straightLabel = straightMode === 'single' ? 'Straight: Single' : 'Straight: Continuous'
265
- const straightIcon = straightMode === 'single' ? 'lucide:minus' : 'lucide:waypoints'
266
- const typeLabel = isCurved ? 'Type: Curved' : 'Type: Straight'
267
- const typeIcon = isCurved ? 'lucide:spline' : 'lucide:minus'
271
+ const typeLabel = isFreehand ? 'Type: Freehand' : isCurved ? 'Type: Curved' : 'Type: Straight'
272
+ const typeIcon = isFreehand ? 'lucide:scribble' : isCurved ? 'lucide:spline' : 'lucide:minus'
273
+ const nextType =
274
+ mode === 'continuous' || mode === 'single'
275
+ ? 'curved'
276
+ : mode === 'curved'
277
+ ? 'freehand'
278
+ : 'continuous'
268
279
 
269
280
  return (
270
281
  <>
271
282
  <ChipRow
272
- ariaLabel={`Fence type: ${isCurved ? 'Curved' : 'Straight'}`}
283
+ ariaLabel={`Fence type: ${typeLabel.replace('Type: ', '')}`}
273
284
  icon={typeIcon}
274
285
  label={typeLabel}
275
- onClick={() => setContinuation('fence', isCurved ? 'continuous' : 'curved')}
286
+ onClick={() => setContinuation('fence', nextType)}
276
287
  shortcut="T"
277
- tooltip="Fence type — click or press T to switch between straight and curved"
288
+ tooltip="Fence type — click or press T to switch between straight, curved and freehand"
278
289
  />
279
290
  <ChipRow
280
- ariaLabel={`Fence continuation: ${straightLabel}`}
281
- disabled={isCurved}
282
- icon={straightIcon}
283
- label={straightLabel}
291
+ ariaLabel={`Fence continuation: ${straightMode === 'single' ? 'Single' : 'Continuous'}`}
292
+ disabled={isCurved || isFreehand}
293
+ icon={straightMode === 'single' ? 'lucide:minus' : 'lucide:waypoints'}
294
+ label={straightMode === 'single' ? 'Straight: Single' : 'Straight: Continuous'}
284
295
  onClick={
285
- isCurved
296
+ isCurved || isFreehand
286
297
  ? undefined
287
298
  : () => setContinuation('fence', straightMode === 'single' ? 'continuous' : 'single')
288
299
  }
289
300
  shortcut="C"
290
301
  tooltip={
291
- isCurved
292
- ? 'Straight continuation is unavailable while curved fence type is active'
302
+ isCurved || isFreehand
303
+ ? 'Straight continuation is unavailable for curved or freehand fences'
293
304
  : 'Straight fence continuation — click or press C to toggle'
294
305
  }
295
306
  />
296
307
  {/* Curved fences are committed by a closing gesture rather than per-click,
297
308
  so the finish keys aren't discoverable on their own — surface them, but
298
309
  only once the user has placed a point and a curve is actually in flight. */}
299
- {isCurved && curveStarted ? (
310
+ {(isCurved || isFreehand) && curveStarted ? (
300
311
  <ChipRow
301
312
  icon="lucide:circle-check"
302
- label="Finish curve (or double-click)"
303
- shortcut="Enter"
313
+ label={isFreehand ? 'Drag to draw fence' : 'Finish curve (or double-click)'}
314
+ shortcut={isFreehand ? 'Release' : 'Enter'}
304
315
  />
305
316
  ) : null}
306
317
  </>
@@ -314,6 +325,15 @@ const PAINT_SCOPE_ICONS: Record<PaintScope, string> = {
314
325
  room: 'lucide:scan',
315
326
  }
316
327
 
328
+ // Why the last paint click did nothing (a surface that has no finish of its
329
+ // own to paint), in the same "!" row the region gestures use for a refusal.
330
+ function PaintNotice() {
331
+ const notice = usePaintRegionMode((s) => s.notice)
332
+ // Leaving the painter forgets it.
333
+ useEffect(() => () => usePaintRegionMode.getState().setNotice(null), [])
334
+ return notice ? <HintRow hint={{ keys: ['!'], label: notice, active: true }} /> : null
335
+ }
336
+
317
337
  // The painter's application-scope chip. Driven entirely by the hovered node's
318
338
  // derived `paintHover` (scopes + labels), so it works for any kind without a
319
339
  // per-target table.
@@ -324,18 +344,31 @@ function PaintScopeChip() {
324
344
  const paintScope = useEditor((s) => s.paintScope)
325
345
  const cyclePaintScope = useEditor((s) => s.cyclePaintScope)
326
346
  const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
327
- const paintEraser = useEditor((s) => s.paintEraser)
347
+ const paintMode = usePaintRegionMode((s) => s.mode)
348
+ const paintEraser = paintMode === 'erase'
349
+ const picking = paintMode === 'pick'
350
+ const verb = picking ? 'Pick' : paintEraser ? 'Erase' : 'Paint'
328
351
 
329
352
  // Nothing to paint with yet (no material picked, not erasing) → the first step
330
353
  // is choosing a material, so say that before anything about scope or hovering.
331
- if (!(paintEraser || hasActivePaintMaterial(activePaintMaterial))) {
354
+ if (!(paintEraser || picking || hasActivePaintMaterial(activePaintMaterial))) {
332
355
  return <ChipRow icon="lucide:palette" label="Select a material to paint" />
333
356
  }
334
357
 
335
358
  // Not over anything paintable → guide the user to hover, still teaching Shift.
336
359
  if (!paintHover) {
337
360
  return (
338
- <ChipRow icon="lucide:mouse-pointer-click" label="Hover a surface to paint" shortcut="Shift" />
361
+ <ChipRow
362
+ icon="lucide:mouse-pointer-click"
363
+ label={
364
+ picking
365
+ ? 'Hover a surface to pick its material'
366
+ : paintEraser
367
+ ? 'Hover a painted surface to erase'
368
+ : 'Hover a surface to paint'
369
+ }
370
+ shortcut={picking ? undefined : 'Shift'}
371
+ />
339
372
  )
340
373
  }
341
374
 
@@ -346,33 +379,81 @@ function PaintScopeChip() {
346
379
 
347
380
  // Paintable but with no scope choice (roof, a one-slot node, …) → a passive
348
381
  // row that still names the surface, so the user always sees what they'll paint.
349
- if (scopes.length <= 1) {
382
+ if (scopes.length <= 1 || picking) {
350
383
  return (
351
384
  <ChipRow
352
- icon={PAINT_SCOPE_ICONS[effective]}
353
- label={`Paint: ${paintScopeLabel(effective, paintHover)}`}
385
+ icon={picking ? 'lucide:pipette' : PAINT_SCOPE_ICONS[effective]}
386
+ label={`${verb}: ${paintScopeLabel(picking ? 'single' : effective, paintHover)}`}
354
387
  />
355
388
  )
356
389
  }
357
390
 
358
391
  return (
359
392
  <ChipRow
360
- ariaLabel={`Paint scope: ${paintScopeLabel(effective, paintHover)}`}
393
+ ariaLabel={`${verb} scope: ${paintScopeLabel(effective, paintHover)}`}
361
394
  icon={PAINT_SCOPE_ICONS[effective]}
362
- label={`Paint: ${paintScopeLabel(effective, paintHover)}`}
395
+ label={`${verb}: ${paintScopeLabel(effective, paintHover)}`}
363
396
  onClick={() => cyclePaintScope()}
364
397
  shortcut="Shift"
365
- tooltip="Paint scope — click or press Shift to cycle"
398
+ tooltip={`${verb} scope — click or press Shift to cycle`}
366
399
  />
367
400
  )
368
401
  }
369
402
 
403
+ // The tool in hand: its icon, name and arming key, over a hairline.
404
+ function HudHeader({ title }: { title: HudTitle }) {
405
+ return (
406
+ <div
407
+ className="col-span-2 mb-0.5 flex items-center gap-2.5 border-border border-b pb-2.5"
408
+ data-hud-title={title.label}
409
+ >
410
+ {title.icon ? (
411
+ <span className="flex size-7 shrink-0 items-center justify-center">
412
+ <IconRefGlyph icon={title.icon} size={26} />
413
+ </span>
414
+ ) : null}
415
+ <span className="min-w-0 flex-1 truncate font-medium text-[13px] text-foreground leading-tight">
416
+ {title.label}
417
+ </span>
418
+ {title.shortcut ? <ShortcutToken className={TOKEN_CLASS} value={title.shortcut} /> : null}
419
+ </div>
420
+ )
421
+ }
422
+
423
+ const isEscHint = (hint: ContextualShortcutHint) =>
424
+ hint.keys.length === 1 && hint.keys[0] === 'Esc'
425
+
426
+ function HintRow({ hint }: { hint: ContextualShortcutHint }) {
427
+ return (
428
+ <div className={cn(ROW_CLASS, 'items-start')}>
429
+ <ShortcutSequence active={hint.active} keys={hint.keys} />
430
+ <div className="min-w-0">
431
+ <div
432
+ className={cn(
433
+ 'text-xs leading-5',
434
+ hint.active ? 'font-medium text-white' : 'text-muted-foreground',
435
+ )}
436
+ >
437
+ {hint.label}
438
+ </div>
439
+ {hint.subtitle ? (
440
+ <div className="text-[10px] text-muted-foreground/70 leading-snug">{hint.subtitle}</div>
441
+ ) : null}
442
+ </div>
443
+ </div>
444
+ )
445
+ }
446
+
447
+ const hintKey = (hint: ContextualShortcutHint) => `${hint.keys.join('+')}:${hint.label}`
448
+
370
449
  export function ContextualHelperPanel({
371
450
  hints,
372
451
  chipHints = [],
373
452
  snapContext = null,
374
453
  showPaintScope = false,
375
454
  continuationContext = null,
455
+ title = null,
456
+ notice = null,
376
457
  }: {
377
458
  hints: ContextualShortcutHint[]
378
459
  // Kind-owned live mode chips (`ToolHint.chip`), rendered alongside the
@@ -383,54 +464,57 @@ export function ContextualHelperPanel({
383
464
  snapContext?: SnapContext | null
384
465
  showPaintScope?: boolean
385
466
  continuationContext?: ContinuationContext | null
467
+ // The tool or gesture in hand, shown as the panel's header.
468
+ title?: HudTitle | null
469
+ // A warning about what is in hand (a floor item in a door's way), in the "!" row.
470
+ notice?: string | null
386
471
  }) {
387
- if (
388
- hints.length === 0 &&
389
- chipHints.length === 0 &&
390
- !snapContext &&
391
- !showPaintScope &&
392
- !continuationContext
472
+ const inStack = useInRightStack()
473
+ const modeChips = chipHints.filter((hint) => hint.chip)
474
+ const hasChips = !!snapContext || !!continuationContext || modeChips.length > 0 || showPaintScope
475
+ const fenceFeature = useEditor((state) =>
476
+ state.mode === 'build' && state.tool === 'fence' ? state.toolDefaults.fence?.featurePlacement : null,
477
+ )
478
+ if (fenceFeature === 'gate' || fenceFeature === 'opening') return (
479
+ <div className={cn(CARD_CLASS, !inStack && FLOATING_CLASS)} data-hud-card>
480
+ {title ? <HudHeader title={title} /> : null}
481
+ <ChipRow shortcut="Left click" label={fenceFeature === 'gate' ? 'Place gate on a fence' : 'Place passage on a fence'} />
482
+ <ChipRow shortcut="Esc" label="Cancel placement" />
483
+ </div>
393
484
  )
394
- return null
485
+ if (hints.length === 0 && !hasChips && !notice) return null
486
+
487
+ const actionHints = hints.filter((hint) => !isEscHint(hint))
488
+ const escHints = hints.filter(isEscHint)
395
489
 
396
490
  return (
397
- <div className={CONTAINER_CLASS}>
491
+ <div
492
+ className={cn(CARD_CLASS, !inStack && FLOATING_CLASS)}
493
+ data-hud-card
494
+ >
495
+ {title ? <HudHeader title={title} /> : null}
496
+ {actionHints.map((hint) => (
497
+ <HintRow hint={hint} key={hintKey(hint)} />
498
+ ))}
499
+ {notice ? <HintRow hint={{ keys: ['!'], label: notice, active: true }} /> : null}
500
+ {actionHints.length > 0 && (hasChips || escHints.length > 0) ? (
501
+ <div className="col-span-2 my-0.5 h-px bg-border" />
502
+ ) : null}
398
503
  {snapContext ? <SnappingChips context={snapContext} /> : null}
399
504
  {continuationContext === 'fence' ? <FenceContinuationChips /> : null}
400
505
  {continuationContext && continuationContext !== 'fence' ? (
401
506
  <ContinuationChip context={continuationContext} />
402
507
  ) : null}
403
- {chipHints.map((hint) =>
404
- hint.chip ? (
405
- <ToolHintChipRow
406
- hint={hint as ToolHint & { chip: NonNullable<ToolHint['chip']> }}
407
- key={`${hint.key}:${hint.label}`}
408
- />
409
- ) : null,
410
- )}
508
+ {modeChips.map((hint) => (
509
+ <ToolHintChipRow
510
+ hint={hint as ToolHint & { chip: NonNullable<ToolHint['chip']> }}
511
+ key={`${hint.key}:${hint.label}`}
512
+ />
513
+ ))}
514
+ {showPaintScope ? <PaintNotice /> : null}
411
515
  {showPaintScope ? <PaintScopeChip /> : null}
412
- {hints.map((hint) => (
413
- <div
414
- className={cn(ROW_CLASS, 'items-start')}
415
- key={`${hint.keys.join('+')}:${hint.label}`}
416
- >
417
- <ShortcutSequence active={hint.active} keys={hint.keys} />
418
- <div className="min-w-0">
419
- <div
420
- className={cn(
421
- 'text-xs leading-5',
422
- hint.active ? 'font-medium text-white' : 'text-muted-foreground',
423
- )}
424
- >
425
- {hint.label}
426
- </div>
427
- {hint.subtitle ? (
428
- <div className="text-[10px] text-muted-foreground/70 leading-snug">
429
- {hint.subtitle}
430
- </div>
431
- ) : null}
432
- </div>
433
- </div>
516
+ {escHints.map((hint) => (
517
+ <HintRow hint={hint} key={hintKey(hint)} />
434
518
  ))}
435
519
  </div>
436
520
  )
@@ -0,0 +1,69 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { SELECT_HUD_TITLE, toolHudTitle } from '../../../lib/hud-title'
4
+ import { snapContextOf } from '../../../lib/snapping-mode'
5
+ import { TooltipProvider } from '../primitives/tooltip'
6
+ import { ContextualHelperPanel } from './contextual-helper-panel'
7
+ import { ROOM_DIVIDE_HINTS } from './helper-manager'
8
+
9
+ describe('Divide HUD', () => {
10
+ test('uses the wall draft snapping context', () => {
11
+ expect(
12
+ snapContextOf({
13
+ scope: { kind: 'room-divide' },
14
+ mode: 'select',
15
+ tool: null,
16
+ profileOf: () => undefined,
17
+ }),
18
+ ).toBe('wall')
19
+ })
20
+
21
+ test('reads like the wall draft: snapping chips and the path keys', () => {
22
+ // Server rendering reads each store's initial state: the wall context starts
23
+ // on Grid with a 0.5 m step.
24
+ const html = renderToStaticMarkup(
25
+ <TooltipProvider>
26
+ <ContextualHelperPanel hints={ROOM_DIVIDE_HINTS} snapContext="wall" />
27
+ </TooltipProvider>,
28
+ )
29
+ expect(html).toContain('Snapping: Grid')
30
+ expect(html).toContain('Grid: 0.50 m')
31
+ for (const label of ['Add point', 'Remove last point', 'Finish at the edge', 'Cancel'])
32
+ expect(html).toContain(label)
33
+ expect(ROOM_DIVIDE_HINTS.map((hint) => hint.keys)).toEqual([
34
+ ['Left click'],
35
+ ['Backspace'],
36
+ ['Enter'],
37
+ ['Esc'],
38
+ ])
39
+ })
40
+ })
41
+
42
+ describe('HUD header', () => {
43
+ test('names the tool in hand with its icon and key, above the gesture rows', () => {
44
+ const html = renderToStaticMarkup(
45
+ <TooltipProvider>
46
+ <ContextualHelperPanel
47
+ hints={[
48
+ { keys: ['Left click'], label: 'Set one corner, then the opposite' },
49
+ { keys: ['Esc'], label: 'Cancel' },
50
+ ]}
51
+ snapContext="wall"
52
+ title={toolHudTitle('wall', 'rectangle')}
53
+ />
54
+ </TooltipProvider>,
55
+ )
56
+ expect(html).toContain('data-hud-title="Rectangle room"')
57
+ expect(html).toContain('src="/icons/room.webp"')
58
+ // Header, then the gesture, then the mode chips, with Esc last.
59
+ const order = ['Rectangle room', 'Set one corner', 'Snapping: Grid', 'Cancel'].map((text) =>
60
+ html.indexOf(text),
61
+ )
62
+ expect(order.every((at) => at >= 0)).toBe(true)
63
+ expect([...order].sort((a, b) => a - b)).toEqual(order)
64
+ })
65
+
66
+ test('select mode is titled Select with its V key', () => {
67
+ expect(SELECT_HUD_TITLE).toMatchObject({ label: 'Select', shortcut: 'V' })
68
+ })
69
+ })