@pascal-app/editor 1.0.3 → 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 (440) hide show
  1. package/package.json +11 -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 +202 -57
  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 +36 -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 +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -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
+ })