@pascal-app/editor 1.0.3 → 1.0.5

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 (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. 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, useId, useState, 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
 
@@ -67,14 +73,16 @@ function ShortcutSequence({
67
73
  <span className="text-[9px] text-muted-foreground/70">/</span>
68
74
  ) : null}
69
75
  <ShortcutToken
70
- className={cn(TOKEN_CLASS, active && 'border-white bg-white text-black shadow-sm')}
76
+ className={cn(TOKEN_CLASS, active &&
77
+ 'border-foreground bg-foreground text-background shadow-sm dark:border-white dark:bg-white dark:text-black')}
71
78
  value={alternative}
72
79
  />
73
80
  </Fragment>
74
81
  ))
75
82
  ) : (
76
83
  <ShortcutToken
77
- className={cn(TOKEN_CLASS, active && 'border-white bg-white text-black shadow-sm')}
84
+ className={cn(TOKEN_CLASS, active &&
85
+ 'border-foreground bg-foreground text-background shadow-sm dark:border-white dark:bg-white dark:text-black')}
78
86
  value={entry}
79
87
  />
80
88
  )}
@@ -260,47 +268,52 @@ function FenceContinuationChips() {
260
268
  const curveStarted = useFenceCurveDraft((s) => s.pointCount > 0)
261
269
 
262
270
  const isCurved = mode === 'curved'
271
+ const isFreehand = mode === 'freehand'
263
272
  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'
273
+ const typeLabel = isFreehand ? 'Type: Freehand' : isCurved ? 'Type: Curved' : 'Type: Straight'
274
+ const typeIcon = isFreehand ? 'lucide:scribble' : isCurved ? 'lucide:spline' : 'lucide:minus'
275
+ const nextType =
276
+ mode === 'continuous' || mode === 'single'
277
+ ? 'curved'
278
+ : mode === 'curved'
279
+ ? 'freehand'
280
+ : 'continuous'
268
281
 
269
282
  return (
270
283
  <>
271
284
  <ChipRow
272
- ariaLabel={`Fence type: ${isCurved ? 'Curved' : 'Straight'}`}
285
+ ariaLabel={`Fence type: ${typeLabel.replace('Type: ', '')}`}
273
286
  icon={typeIcon}
274
287
  label={typeLabel}
275
- onClick={() => setContinuation('fence', isCurved ? 'continuous' : 'curved')}
288
+ onClick={() => setContinuation('fence', nextType)}
276
289
  shortcut="T"
277
- tooltip="Fence type — click or press T to switch between straight and curved"
290
+ tooltip="Fence type — click or press T to switch between straight, curved and freehand"
278
291
  />
279
292
  <ChipRow
280
- ariaLabel={`Fence continuation: ${straightLabel}`}
281
- disabled={isCurved}
282
- icon={straightIcon}
283
- label={straightLabel}
293
+ ariaLabel={`Fence continuation: ${straightMode === 'single' ? 'Single' : 'Continuous'}`}
294
+ disabled={isCurved || isFreehand}
295
+ icon={straightMode === 'single' ? 'lucide:minus' : 'lucide:waypoints'}
296
+ label={straightMode === 'single' ? 'Straight: Single' : 'Straight: Continuous'}
284
297
  onClick={
285
- isCurved
298
+ isCurved || isFreehand
286
299
  ? undefined
287
300
  : () => setContinuation('fence', straightMode === 'single' ? 'continuous' : 'single')
288
301
  }
289
302
  shortcut="C"
290
303
  tooltip={
291
- isCurved
292
- ? 'Straight continuation is unavailable while curved fence type is active'
304
+ isCurved || isFreehand
305
+ ? 'Straight continuation is unavailable for curved or freehand fences'
293
306
  : 'Straight fence continuation — click or press C to toggle'
294
307
  }
295
308
  />
296
309
  {/* Curved fences are committed by a closing gesture rather than per-click,
297
310
  so the finish keys aren't discoverable on their own — surface them, but
298
311
  only once the user has placed a point and a curve is actually in flight. */}
299
- {isCurved && curveStarted ? (
312
+ {(isCurved || isFreehand) && curveStarted ? (
300
313
  <ChipRow
301
314
  icon="lucide:circle-check"
302
- label="Finish curve (or double-click)"
303
- shortcut="Enter"
315
+ label={isFreehand ? 'Drag to draw fence' : 'Finish curve (or double-click)'}
316
+ shortcut={isFreehand ? 'Release' : 'Enter'}
304
317
  />
305
318
  ) : null}
306
319
  </>
@@ -314,6 +327,15 @@ const PAINT_SCOPE_ICONS: Record<PaintScope, string> = {
314
327
  room: 'lucide:scan',
315
328
  }
316
329
 
330
+ // Why the last paint click did nothing (a surface that has no finish of its
331
+ // own to paint), in the same "!" row the region gestures use for a refusal.
332
+ function PaintNotice() {
333
+ const notice = usePaintRegionMode((s) => s.notice)
334
+ // Leaving the painter forgets it.
335
+ useEffect(() => () => usePaintRegionMode.getState().setNotice(null), [])
336
+ return notice ? <HintRow hint={{ keys: ['!'], label: notice, active: true }} /> : null
337
+ }
338
+
317
339
  // The painter's application-scope chip. Driven entirely by the hovered node's
318
340
  // derived `paintHover` (scopes + labels), so it works for any kind without a
319
341
  // per-target table.
@@ -324,18 +346,31 @@ function PaintScopeChip() {
324
346
  const paintScope = useEditor((s) => s.paintScope)
325
347
  const cyclePaintScope = useEditor((s) => s.cyclePaintScope)
326
348
  const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
327
- const paintEraser = useEditor((s) => s.paintEraser)
349
+ const paintMode = usePaintRegionMode((s) => s.mode)
350
+ const paintEraser = paintMode === 'erase'
351
+ const picking = paintMode === 'pick'
352
+ const verb = picking ? 'Pick' : paintEraser ? 'Erase' : 'Paint'
328
353
 
329
354
  // Nothing to paint with yet (no material picked, not erasing) → the first step
330
355
  // is choosing a material, so say that before anything about scope or hovering.
331
- if (!(paintEraser || hasActivePaintMaterial(activePaintMaterial))) {
356
+ if (!(paintEraser || picking || hasActivePaintMaterial(activePaintMaterial))) {
332
357
  return <ChipRow icon="lucide:palette" label="Select a material to paint" />
333
358
  }
334
359
 
335
360
  // Not over anything paintable → guide the user to hover, still teaching Shift.
336
361
  if (!paintHover) {
337
362
  return (
338
- <ChipRow icon="lucide:mouse-pointer-click" label="Hover a surface to paint" shortcut="Shift" />
363
+ <ChipRow
364
+ icon="lucide:mouse-pointer-click"
365
+ label={
366
+ picking
367
+ ? 'Hover a surface to pick its material'
368
+ : paintEraser
369
+ ? 'Hover a painted surface to erase'
370
+ : 'Hover a surface to paint'
371
+ }
372
+ shortcut={picking ? undefined : 'Shift'}
373
+ />
339
374
  )
340
375
  }
341
376
 
@@ -346,33 +381,85 @@ function PaintScopeChip() {
346
381
 
347
382
  // Paintable but with no scope choice (roof, a one-slot node, …) → a passive
348
383
  // row that still names the surface, so the user always sees what they'll paint.
349
- if (scopes.length <= 1) {
384
+ if (scopes.length <= 1 || picking) {
350
385
  return (
351
386
  <ChipRow
352
- icon={PAINT_SCOPE_ICONS[effective]}
353
- label={`Paint: ${paintScopeLabel(effective, paintHover)}`}
387
+ icon={picking ? 'lucide:pipette' : PAINT_SCOPE_ICONS[effective]}
388
+ label={`${verb}: ${paintScopeLabel(picking ? 'single' : effective, paintHover)}`}
354
389
  />
355
390
  )
356
391
  }
357
392
 
358
393
  return (
359
394
  <ChipRow
360
- ariaLabel={`Paint scope: ${paintScopeLabel(effective, paintHover)}`}
395
+ ariaLabel={`${verb} scope: ${paintScopeLabel(effective, paintHover)}`}
361
396
  icon={PAINT_SCOPE_ICONS[effective]}
362
- label={`Paint: ${paintScopeLabel(effective, paintHover)}`}
397
+ label={`${verb}: ${paintScopeLabel(effective, paintHover)}`}
363
398
  onClick={() => cyclePaintScope()}
364
399
  shortcut="Shift"
365
- tooltip="Paint scope — click or press Shift to cycle"
400
+ tooltip={`${verb} scope — click or press Shift to cycle`}
366
401
  />
367
402
  )
368
403
  }
369
404
 
405
+ // The tool in hand: its icon, name and arming key, over a hairline.
406
+ function HudHeader({ title }: { title: HudTitle }) {
407
+ return (
408
+ <div
409
+ className="col-span-2 mb-0.5 flex items-center gap-2.5 border-border border-b pb-2.5"
410
+ data-hud-title={title.label}
411
+ >
412
+ {title.icon ? (
413
+ <span className="flex size-7 shrink-0 items-center justify-center">
414
+ <IconRefGlyph icon={title.icon} size={26} />
415
+ </span>
416
+ ) : null}
417
+ <span className="min-w-0 flex-1 truncate font-medium text-[13px] text-foreground leading-tight">
418
+ {title.label}
419
+ </span>
420
+ {title.shortcut ? <ShortcutToken className={TOKEN_CLASS} value={title.shortcut} /> : null}
421
+ </div>
422
+ )
423
+ }
424
+
425
+ const isEscHint = (hint: ContextualShortcutHint) =>
426
+ hint.keys.length === 1 && hint.keys[0] === 'Esc'
427
+
428
+ // A "!" row says why something won't happen here (a wall end a window can't wrap), not a key to
429
+ // press, so it is never folded under More shortcuts.
430
+ const isNoteHint = (hint: ContextualShortcutHint) => hint.keys.length === 1 && hint.keys[0] === '!'
431
+
432
+ function HintRow({ hint }: { hint: ContextualShortcutHint }) {
433
+ return (
434
+ <div className={cn(ROW_CLASS, 'items-start')}>
435
+ <ShortcutSequence active={hint.active} keys={hint.keys} />
436
+ <div className="min-w-0">
437
+ <div
438
+ className={cn(
439
+ 'text-xs leading-5',
440
+ hint.active ? 'font-medium text-foreground dark:text-white' : 'text-muted-foreground',
441
+ )}
442
+ >
443
+ {hint.label}
444
+ </div>
445
+ {hint.subtitle ? (
446
+ <div className="text-[10px] text-muted-foreground/70 leading-snug">{hint.subtitle}</div>
447
+ ) : null}
448
+ </div>
449
+ </div>
450
+ )
451
+ }
452
+
453
+ const hintKey = (hint: ContextualShortcutHint) => `${hint.keys.join('+')}:${hint.label}`
454
+
370
455
  export function ContextualHelperPanel({
371
456
  hints,
372
457
  chipHints = [],
373
458
  snapContext = null,
374
459
  showPaintScope = false,
375
460
  continuationContext = null,
461
+ title = null,
462
+ notice = null,
376
463
  }: {
377
464
  hints: ContextualShortcutHint[]
378
465
  // Kind-owned live mode chips (`ToolHint.chip`), rendered alongside the
@@ -383,54 +470,76 @@ export function ContextualHelperPanel({
383
470
  snapContext?: SnapContext | null
384
471
  showPaintScope?: boolean
385
472
  continuationContext?: ContinuationContext | null
473
+ // The tool or gesture in hand, shown as the panel's header.
474
+ title?: HudTitle | null
475
+ // A warning about what is in hand (a floor item in a door's way), in the "!" row.
476
+ notice?: string | null
386
477
  }) {
387
- if (
388
- hints.length === 0 &&
389
- chipHints.length === 0 &&
390
- !snapContext &&
391
- !showPaintScope &&
392
- !continuationContext
478
+ const inStack = useInRightStack()
479
+ const [showAllHints, setShowAllHints] = useState(false)
480
+ const hintsId = useId()
481
+ const modeChips = chipHints.filter((hint) => hint.chip)
482
+ const hasChips = !!snapContext || !!continuationContext || modeChips.length > 0 || showPaintScope
483
+ const fenceFeature = useEditor((state) =>
484
+ state.mode === 'build' && state.tool === 'fence' ? state.toolDefaults.fence?.featurePlacement : null,
393
485
  )
394
- return null
486
+ if (fenceFeature === 'gate' || fenceFeature === 'opening') return (
487
+ <div className={cn(CARD_CLASS, !inStack && FLOATING_CLASS)} data-hud-card>
488
+ {title ? <HudHeader title={title} /> : null}
489
+ <ChipRow shortcut="Left click" label={fenceFeature === 'gate' ? 'Place gate on a fence' : 'Place passage on a fence'} />
490
+ <ChipRow shortcut="Esc" label="Cancel placement" />
491
+ </div>
492
+ )
493
+ if (hints.length === 0 && !hasChips && !notice) return null
494
+
495
+ const actionHints = hints.filter((hint) => !isEscHint(hint))
496
+ const escHints = hints.filter(isEscHint)
497
+ const collapsedHints = actionHints.filter(
498
+ (hint, index) => index === 0 || hint.active || isNoteHint(hint),
499
+ )
500
+ const visibleHints = showAllHints ? actionHints : collapsedHints
395
501
 
396
502
  return (
397
- <div className={CONTAINER_CLASS}>
503
+ <div
504
+ className={cn(CARD_CLASS, !inStack && FLOATING_CLASS)}
505
+ data-hud-card
506
+ >
507
+ {title ? <HudHeader title={title} /> : null}
508
+ <div id={hintsId} className="col-span-2 grid grid-cols-subgrid gap-y-1.5">
509
+ {visibleHints.map((hint) => (
510
+ <HintRow hint={hint} key={hintKey(hint)} />
511
+ ))}
512
+ </div>
513
+ {collapsedHints.length < actionHints.length ? (
514
+ <button
515
+ aria-controls={hintsId}
516
+ aria-expanded={showAllHints}
517
+ className="pointer-events-auto col-span-2 min-h-7 rounded-md px-1 text-left text-[11px] text-muted-foreground hover:bg-muted/60 hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring"
518
+ onClick={() => setShowAllHints((value) => !value)}
519
+ type="button"
520
+ >
521
+ {showAllHints ? 'Fewer shortcuts' : 'More shortcuts'}
522
+ </button>
523
+ ) : null}
524
+ {notice ? <HintRow hint={{ keys: ['!'], label: notice, active: true }} /> : null}
525
+ {actionHints.length > 0 && (hasChips || escHints.length > 0) ? (
526
+ <div className="col-span-2 my-0.5 h-px bg-border" />
527
+ ) : null}
398
528
  {snapContext ? <SnappingChips context={snapContext} /> : null}
399
529
  {continuationContext === 'fence' ? <FenceContinuationChips /> : null}
400
530
  {continuationContext && continuationContext !== 'fence' ? (
401
531
  <ContinuationChip context={continuationContext} />
402
532
  ) : 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
- )}
533
+ {modeChips.map((hint) => (
534
+ <ToolHintChipRow
535
+ hint={hint as ToolHint & { chip: NonNullable<ToolHint['chip']> }}
536
+ key={`${hint.key}:${hint.label}`}
537
+ />
538
+ ))}
539
+ {showPaintScope ? <PaintNotice /> : null}
411
540
  {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>
541
+ {escHints.map((hint) => (
542
+ <HintRow hint={hint} key={hintKey(hint)} />
434
543
  ))}
435
544
  </div>
436
545
  )
@@ -0,0 +1,121 @@
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 { cornerHintRows } from '../../../store/use-corner-snap-hint'
6
+ import { TooltipProvider } from '../primitives/tooltip'
7
+ import { ContextualHelperPanel } from './contextual-helper-panel'
8
+ import { ROOM_DIVIDE_HINTS } from './helper-manager'
9
+
10
+ describe('Divide HUD', () => {
11
+ test('uses the wall draft snapping context', () => {
12
+ expect(
13
+ snapContextOf({
14
+ scope: { kind: 'room-divide' },
15
+ mode: 'select',
16
+ tool: null,
17
+ profileOf: () => undefined,
18
+ }),
19
+ ).toBe('wall')
20
+ })
21
+
22
+ test('shows snapping controls and the primary gesture with expandable secondary shortcuts', () => {
23
+ // Server rendering reads each store's initial state: the wall context starts
24
+ // on Grid with a 0.5 m step.
25
+ const html = renderToStaticMarkup(
26
+ <TooltipProvider>
27
+ <ContextualHelperPanel hints={ROOM_DIVIDE_HINTS} snapContext="wall" />
28
+ </TooltipProvider>,
29
+ )
30
+ expect(html).toContain('Snapping: Grid')
31
+ expect(html).toContain('Grid: 0.50 m')
32
+ for (const label of ['Add point', 'More shortcuts', 'Cancel'])
33
+ expect(html).toContain(label)
34
+ expect(html).toContain('aria-expanded="false"')
35
+ for (const label of ['Remove last point', 'Finish at the edge'])
36
+ expect(html).not.toContain(label)
37
+ expect(ROOM_DIVIDE_HINTS.map((hint) => hint.keys)).toEqual([
38
+ ['Left click'],
39
+ ['Backspace'],
40
+ ['Enter'],
41
+ ['Esc'],
42
+ ])
43
+ })
44
+
45
+ test('keeps active secondary gestures visible while shortcuts are collapsed', () => {
46
+ const html = renderToStaticMarkup(
47
+ <TooltipProvider>
48
+ <ContextualHelperPanel
49
+ hints={ROOM_DIVIDE_HINTS.map((hint) => ({
50
+ ...hint,
51
+ active: hint.keys.includes('Enter'),
52
+ }))}
53
+ snapContext="wall"
54
+ />
55
+ </TooltipProvider>,
56
+ )
57
+ for (const label of ['Add point', 'Finish at the edge', 'Cancel'])
58
+ expect(html).toContain(label)
59
+ expect(html).not.toContain('Remove last point')
60
+ expect(html).toContain('aria-expanded="false"')
61
+ })
62
+
63
+ test('keeps a note row visible while shortcuts are collapsed', () => {
64
+ const html = renderToStaticMarkup(
65
+ <TooltipProvider>
66
+ <ContextualHelperPanel
67
+ hints={[
68
+ { keys: ['Left click'], label: 'Place' },
69
+ ...cornerHintRows('free_end'),
70
+ { keys: ['R', 'T'], label: 'Rotate' },
71
+ { keys: ['Esc'], label: 'Cancel' },
72
+ ]}
73
+ />
74
+ </TooltipProvider>,
75
+ )
76
+ for (const label of ['Place', 'No wall meets here', 'More shortcuts', 'Cancel'])
77
+ expect(html).toContain(label)
78
+ expect(html).not.toContain('Rotate')
79
+ })
80
+
81
+ test('offers More shortcuts only when collapsing hides a row', () => {
82
+ const html = renderToStaticMarkup(
83
+ <TooltipProvider>
84
+ <ContextualHelperPanel
85
+ hints={[{ keys: ['Left click'], label: 'Place' }, ...cornerHintRows('free_end')]}
86
+ />
87
+ </TooltipProvider>,
88
+ )
89
+ expect(html).toContain('No wall meets here')
90
+ expect(html).not.toContain('More shortcuts')
91
+ })
92
+ })
93
+
94
+ describe('HUD header', () => {
95
+ test('names the tool in hand with its icon and key, above the gesture rows', () => {
96
+ const html = renderToStaticMarkup(
97
+ <TooltipProvider>
98
+ <ContextualHelperPanel
99
+ hints={[
100
+ { keys: ['Left click'], label: 'Set one corner, then the opposite' },
101
+ { keys: ['Esc'], label: 'Cancel' },
102
+ ]}
103
+ snapContext="wall"
104
+ title={toolHudTitle('wall', 'rectangle')}
105
+ />
106
+ </TooltipProvider>,
107
+ )
108
+ expect(html).toContain('data-hud-title="Rectangle room"')
109
+ expect(html).toContain('src="/icons/room.webp"')
110
+ // Header, then the gesture, then the mode chips, with Esc last.
111
+ const order = ['Rectangle room', 'Set one corner', 'Snapping: Grid', 'Cancel'].map((text) =>
112
+ html.indexOf(text),
113
+ )
114
+ expect(order.every((at) => at >= 0)).toBe(true)
115
+ expect([...order].sort((a, b) => a - b)).toEqual(order)
116
+ })
117
+
118
+ test('select mode is titled Select with its V key', () => {
119
+ expect(SELECT_HUD_TITLE).toMatchObject({ label: 'Select', shortcut: 'V' })
120
+ })
121
+ })