@pascal-app/editor 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -3,6 +3,7 @@
3
3
  import {
4
4
  type AnyNode,
5
5
  type AnyNodeId,
6
+ floorPlacedCollides,
6
7
  nodeRegistry,
7
8
  type TerrainVerb,
8
9
  useScene,
@@ -20,18 +21,60 @@ import {
20
21
  resolveSelectModeHelpHints,
21
22
  } from '../../../lib/contextual-help'
22
23
  import { getContextualHelpNodeExtension } from '../../../lib/contextual-help-extension'
23
- import { continuationContextOf } from '../../../lib/continuation'
24
+ import { continuationContextOf, keyCyclableContinuationContext } from '../../../lib/continuation'
24
25
  import { canDirectMoveNode, canDirectRotateNode } from '../../../lib/direct-manipulation'
26
+ import {
27
+ DIVIDE_ROOM_HUD_TITLE,
28
+ MEZZANINE_HUD_TITLE,
29
+ OPENING_HUD_TITLE,
30
+ TERRACE_HUD_TITLE,
31
+ MOVE_ROOM_HUD_TITLE,
32
+ MOVE_SELECTION_HUD_TITLE,
33
+ nodeKindHudTitle,
34
+ ERASE_HUD_TITLE,
35
+ PAINT_HUD_TITLE,
36
+ PICK_MATERIAL_HUD_TITLE,
37
+ paintRegionHudTitle,
38
+ RESIZE_HUD_TITLE,
39
+ ROTATE_HUD_TITLE,
40
+ SELECT_HUD_TITLE,
41
+ SMART_MEASURE_HUD_TITLE,
42
+ terrainHudTitle,
43
+ toolHudTitle,
44
+ } from '../../../lib/hud-title'
25
45
  import type { ReshapeKind } from '../../../lib/interaction/scope'
46
+ import {
47
+ type MezzanineShape,
48
+ useMezzanineDraft,
49
+ } from '../../../lib/mezzanine-draft'
50
+ import {
51
+ TERRACE_SHAPE_HINT,
52
+ type TerraceShape,
53
+ useTerraceDraft,
54
+ } from '../../../lib/terrace-draft'
55
+ import {
56
+ type OpeningShape,
57
+ useOpeningDraft,
58
+ } from '../../../lib/floor-opening-draft'
26
59
  import { isFreshPlacementMetadata } from '../../../lib/placement-metadata'
60
+ import { MEZZANINE_EDGE_DRAG_LABEL, ROOM_ELEVATION_DRAG_LABEL } from '../../../lib/room-handle-drag'
61
+ import { ROOM_MOVE_DRAG_LABEL } from '../../../lib/room-transform-session'
62
+ import { usePaintRegionHovering } from '../../../lib/paint-region-hover'
63
+ import {
64
+ type PaintMode,
65
+ paintRegionHoverHint,
66
+ usePaintRegionMode,
67
+ } from '../../../lib/paint-region-mode'
27
68
  import { snapContextOf } from '../../../lib/snapping-mode'
28
69
  import useEditor, { getActiveContinuationContext } from '../../../store/use-editor'
29
70
  import useInteractionScope, {
30
71
  useActiveHandleDrag,
31
72
  useMovingNode,
73
+ useReshapingNode,
32
74
  } from '../../../store/use-interaction-scope'
33
75
  import { BuildingHelper } from './building-helper'
34
76
  import { ContextualHelperPanel } from './contextual-helper-panel'
77
+ import { transientDraftId, usePlacementNotice } from '../../../hooks/use-placement-notice'
35
78
  import { ItemHelper } from './item-helper'
36
79
  import { RegisteredToolHelper } from './registered-tool-helper'
37
80
 
@@ -84,15 +127,100 @@ function terrainSculptHints(verb: TerrainVerb, sampling: boolean): ContextualSho
84
127
  }
85
128
 
86
129
  type ActiveModifierKeys = {
130
+ alt: boolean
87
131
  command: boolean
88
132
  shift: boolean
89
133
  }
90
134
 
91
135
  const EMPTY_CONTEXTUAL_HINTS: ContextualShortcutHint[] = []
136
+
137
+ // The paint tool's region sub-modes: what one gesture draws, then Esc. A
138
+ // refused gesture (the per-face cap) shows as the first row.
139
+ /** What a click does in each whole-surface sub-mode; only painting teaches the held eyedropper. */
140
+ export function paintHints(mode: PaintMode): ContextualShortcutHint[] {
141
+ if (mode === 'pick')
142
+ return [
143
+ { keys: ['Left click'], label: 'Paint with this material' },
144
+ { keys: ['Esc'], label: 'Cancel' },
145
+ ]
146
+ if (mode === 'erase')
147
+ return [{ keys: ['Left click'], label: 'Remove a painted part, or reset a surface' }]
148
+ if (mode === 'surface') return [{ keys: [['Alt', 'Option']], label: 'Hold to pick a material' }]
149
+ return []
150
+ }
151
+
152
+ export function paintRegionHints(
153
+ mode: 'rectangle' | 'polygon',
154
+ notice: string | null,
155
+ hovering = true,
156
+ ): ContextualShortcutHint[] {
157
+ const gesture: Record<typeof mode, ContextualShortcutHint> = {
158
+ rectangle: { keys: ['Drag'], label: 'Press anywhere, drag to the opposite corner' },
159
+ polygon: { keys: ['Left click'], label: 'Add point · click the first to close' },
160
+ }
161
+ // Off anything the sub-mode draws on, the HUD says where to go instead.
162
+ if (!hovering)
163
+ return [
164
+ ...(notice ? [{ keys: ['!'], label: notice, active: true }] : []),
165
+ { keys: ['Hover'], label: paintRegionHoverHint(mode), active: true },
166
+ { keys: ['Esc'], label: 'Cancel' },
167
+ ]
168
+ return [
169
+ ...(notice ? [{ keys: ['!'], label: notice, active: true }] : []),
170
+ gesture[mode],
171
+ ...(mode === 'polygon' ? [{ keys: ['Backspace'], label: 'Remove last point' }] : []),
172
+ { keys: ['Esc'], label: 'Cancel' },
173
+ ]
174
+ }
175
+ // "Add mezzanine": the outline gesture for the shape picked in the room panel,
176
+ // then Esc; the snapping chip sits below.
177
+ export function mezzanineHints(shape: MezzanineShape): ContextualShortcutHint[] {
178
+ return shape === 'rectangle'
179
+ ? [
180
+ { keys: ['Left click'], label: 'Set one corner, then the opposite' },
181
+ { keys: ['Drag'], label: 'Draw the mezzanine' },
182
+ { keys: ['Esc'], label: 'Cancel' },
183
+ ]
184
+ : [
185
+ { keys: ['Left click'], label: 'Add point · click the first to close' },
186
+ { keys: ['Backspace'], label: 'Remove last point' },
187
+ { keys: ['Enter'], label: 'Finish' },
188
+ { keys: ['Esc'], label: 'Cancel' },
189
+ ]
190
+ }
191
+ /** "Cut opening": the outline gesture for the chosen shape, then Esc. */
192
+ export function openingHints(shape: OpeningShape): ContextualShortcutHint[] {
193
+ return mezzanineHints(shape).map((hint) =>
194
+ hint.label === 'Draw the mezzanine' ? { ...hint, label: 'Draw the opening' } : hint,
195
+ )
196
+ }
197
+ const TERRACE_CHIP_HINTS = [TERRACE_SHAPE_HINT]
198
+ // The terrace tool: the outline gesture for the chosen shape, then Esc.
199
+ export function terraceHints(shape: TerraceShape): ContextualShortcutHint[] {
200
+ return shape === 'rectangle'
201
+ ? [
202
+ { keys: ['Left click'], label: 'Set one corner, then the opposite' },
203
+ { keys: ['Drag'], label: 'Draw the terrace' },
204
+ { keys: ['Esc'], label: 'Done' },
205
+ ]
206
+ : [
207
+ { keys: ['Left click'], label: 'Add point · click the first to close' },
208
+ { keys: ['Backspace'], label: 'Remove last point' },
209
+ { keys: ['Enter'], label: 'Finish' },
210
+ { keys: ['Esc'], label: 'Done' },
211
+ ]
212
+ }
213
+ export const ROOM_DIVIDE_HINTS: ContextualShortcutHint[] = [
214
+ { keys: ['Left click'], label: 'Add point' },
215
+ { keys: ['Backspace'], label: 'Remove last point' },
216
+ { keys: ['Enter'], label: 'Finish at the edge' },
217
+ { keys: ['Esc'], label: 'Cancel' },
218
+ ]
92
219
  const NO_CONTEXTUAL_HELP_SUBSCRIPTION = () => () => {}
93
220
 
94
221
  function useActiveModifierKeys(): ActiveModifierKeys {
95
222
  const [modifiers, setModifiers] = useState<ActiveModifierKeys>({
223
+ alt: false,
96
224
  command: false,
97
225
  shift: false,
98
226
  })
@@ -101,6 +229,7 @@ function useActiveModifierKeys(): ActiveModifierKeys {
101
229
  const updateModifiers = (event: KeyboardEvent) => {
102
230
  const isKeyDown = event.type === 'keydown'
103
231
  setModifiers({
232
+ alt: event.altKey || (isKeyDown && event.key === 'Alt'),
104
233
  command:
105
234
  event.metaKey ||
106
235
  event.ctrlKey ||
@@ -109,7 +238,7 @@ function useActiveModifierKeys(): ActiveModifierKeys {
109
238
  })
110
239
  }
111
240
  const clearModifiers = () => {
112
- setModifiers({ command: false, shift: false })
241
+ setModifiers({ alt: false, command: false, shift: false })
113
242
  }
114
243
 
115
244
  window.addEventListener('keydown', updateModifiers)
@@ -125,6 +254,43 @@ function useActiveModifierKeys(): ActiveModifierKeys {
125
254
  return modifiers
126
255
  }
127
256
 
257
+ export type MezzanineGesture = 'raise' | 'resize' | 'move'
258
+
259
+ /**
260
+ * A handle drag on a mezzanine — its floor handle, an edge arrow, or its pick-up
261
+ * (Move / Duplicate) — or null. Its HUD names the mezzanine, not "Resize" or
262
+ * "Move room".
263
+ */
264
+ export function mezzanineGesture(
265
+ drag: { nodeId: string; label: string } | null,
266
+ nodes: Readonly<Record<string, AnyNode>>,
267
+ ): MezzanineGesture | null {
268
+ if (!drag) return null
269
+ const zone = nodes[drag.nodeId]
270
+ if (zone?.type !== 'zone' || zone.floor?.support !== 'open') return null
271
+ if (drag.label === ROOM_ELEVATION_DRAG_LABEL) return 'raise'
272
+ if (drag.label === MEZZANINE_EDGE_DRAG_LABEL) return 'resize'
273
+ if (drag.label === ROOM_MOVE_DRAG_LABEL) return 'move'
274
+ return null
275
+ }
276
+
277
+ export const MEZZANINE_GESTURE_HINTS: Record<MezzanineGesture, ContextualShortcutHint[]> = {
278
+ raise: [
279
+ { keys: ['Drag'], label: 'Raise or lower' },
280
+ { keys: ['Esc'], label: 'Cancel' },
281
+ ],
282
+ resize: [
283
+ { keys: ['Drag'], label: 'Resize' },
284
+ { keys: ['Esc'], label: 'Cancel' },
285
+ ],
286
+ move: [
287
+ { keys: ['Left click'], label: 'Place inside the room' },
288
+ { keys: [['R', 'T']], label: 'Rotate ±45°' },
289
+ { keys: ['Alt'], label: 'Free move' },
290
+ { keys: ['Esc'], label: 'Cancel' },
291
+ ],
292
+ }
293
+
128
294
  export function HelperManager() {
129
295
  const mode = useEditor((s) => s.mode)
130
296
  const tool = useEditor((s) => s.tool)
@@ -132,10 +298,24 @@ export function HelperManager() {
132
298
  const terrainSampling = useEditor((s) => s.terrainSampling)
133
299
  const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
134
300
  const measurementToolKind = useEditor((s) => s.toolDefaults.measurement?.kind)
301
+ const wallMode = useEditor((s) => s.continuationByContext.wall)
135
302
  const workspaceMode = useEditor((s) => s.workspaceMode)
136
303
  const scope = useInteractionScope((s) => s.scope)
304
+ const paintRegionMode = usePaintRegionMode((s) => s.mode)
305
+ const paintRegionDrawing = paintRegionMode === 'rectangle' || paintRegionMode === 'polygon'
306
+ const paintRegionNotice = usePaintRegionMode((s) => s.notice)
307
+ const paintRegionHovering = usePaintRegionHovering()
308
+ const mezzanineShape = useMezzanineDraft((s) => (s.host ? s.shape : null))
309
+ const openingShape = useOpeningDraft((s) => (s.host ? s.shape : null))
310
+ const terraceShape = useTerraceDraft((s) => (s.host ? s.shape : null))
137
311
  const movingNode = useMovingNode()
312
+ // A tool whose kind gives placement notices: its transient draft is the node in hand.
313
+ const toolNotices = !!(tool && nodeRegistry.get(tool)?.placementNotice)
314
+ const toolDraftId = useScene((s) => (toolNotices ? transientDraftId(s.nodes) : null))
315
+ const placementNotice = usePlacementNotice(movingNode?.id ?? toolDraftId)
316
+ const reshapingNode = useReshapingNode()
138
317
  const activeHandleDrag = useActiveHandleDrag()
318
+ const mezzanineHandle = useScene((s) => mezzanineGesture(activeHandleDrag, s.nodes))
139
319
  const selectedIds = useViewer((s) => s.selection.selectedIds)
140
320
  const isMobile = useIsMobile()
141
321
  const modifiers = useActiveModifierKeys()
@@ -175,11 +355,14 @@ export function HelperManager() {
175
355
  return node ? nodeRegistry.get(node.type)?.snapProfile : undefined
176
356
  },
177
357
  draftDirectionalOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapDraftDirectional ?? true,
358
+ paintRegion: mode === 'material-paint' && paintRegionDrawing,
178
359
  }),
179
- [scope, mode, tool],
360
+ [scope, mode, tool, paintRegionDrawing],
180
361
  )
362
+ // Contexts whose mode is picked in the Build panel (the wall's Rooms variant)
363
+ // get no HUD chip: the header names the variant instead.
181
364
  const continuationContext = useMemo(
182
- () => getActiveContinuationContext(),
365
+ () => keyCyclableContinuationContext(getActiveContinuationContext()),
183
366
  [scope, mode, tool],
184
367
  )
185
368
  const selectModeHints = useMemo(() => {
@@ -190,10 +373,12 @@ export function HelperManager() {
190
373
  : single?.type === 'duct-fitting' || single?.type === 'pipe-fitting'
191
374
  ? 'fitting'
192
375
  : null
376
+ const hasOpeningRadiusSelection = single?.type === 'door' || single?.type === 'window'
193
377
  return resolveSelectModeHelpHints({
194
378
  selectedCount: selectedNodes.length,
195
379
  hasMovableSelection: selectedNodes.some((node) => canDirectMoveNode(node)),
196
380
  hasRotatableSelection: selectedNodes.some((node) => canDirectRotateNode(node)),
381
+ hasOpeningRadiusSelection,
197
382
  commandPressed: modifiers.command,
198
383
  shiftPressed: modifiers.shift,
199
384
  mepSelection,
@@ -211,15 +396,17 @@ export function HelperManager() {
211
396
  // tools — editor shortcut hints would only mislead there.
212
397
  if (workspaceMode === 'studio') return null
213
398
 
214
- // Rotating a node (or a multi-selection group) via its in-world gizmo:
215
- // advertise Shift = free rotation, the same angle-step bypass wall drafting
216
- // exposes. Takes priority over the idle select-mode hints since a handle
217
- // drag is the active interaction.
218
399
  if (
219
400
  activeHandleDrag?.label === ROTATE_HANDLE_DRAG_LABEL ||
220
401
  activeHandleDrag?.label === GROUP_ROTATE_DRAG_LABEL
221
402
  ) {
222
- return <ContextualHelperPanel hints={resolveRotateHandleHelpHints(modifiers.shift)} />
403
+ return (
404
+ <ContextualHelperPanel
405
+ hints={resolveRotateHandleHelpHints(modifiers.alt)}
406
+ snapContext={snapContext}
407
+ title={ROTATE_HUD_TITLE}
408
+ />
409
+ )
223
410
  }
224
411
 
225
412
  // Group-move drag / pick-up: the drag resolves to the 'item' snap context
@@ -230,6 +417,79 @@ export function HelperManager() {
230
417
  <ContextualHelperPanel
231
418
  hints={[{ keys: ['R / T'], label: 'Rotate the selection ±45°' }]}
232
419
  snapContext={snapContext}
420
+ title={MOVE_SELECTION_HUD_TITLE}
421
+ />
422
+ )
423
+ }
424
+
425
+ if (mezzanineHandle) {
426
+ return (
427
+ <ContextualHelperPanel
428
+ hints={MEZZANINE_GESTURE_HINTS[mezzanineHandle]}
429
+ snapContext={snapContext}
430
+ title={MEZZANINE_HUD_TITLE}
431
+ />
432
+ )
433
+ }
434
+
435
+ // A picked-up room (Move / Duplicate / Rotate from its pill): the same keys
436
+ // as a group pick-up.
437
+ if (activeHandleDrag?.label === ROOM_MOVE_DRAG_LABEL) {
438
+ return (
439
+ <ContextualHelperPanel
440
+ hints={[
441
+ { keys: ['Left click'], label: 'Place' },
442
+ { keys: [['R', 'T']], label: 'Rotate ±45°' },
443
+ { keys: ['Alt'], label: 'Free move · slide doors and windows' },
444
+ { keys: ['Esc'], label: 'Cancel' },
445
+ ]}
446
+ snapContext={snapContext}
447
+ title={MOVE_ROOM_HUD_TITLE}
448
+ />
449
+ )
450
+ }
451
+ // A paint region gesture holds a handle-drag scope while it draws; its HUD
452
+ // stays the region one (Backspace / close hints for a polygon mid-draft).
453
+ if (mode === 'material-paint' && paintRegionDrawing) {
454
+ return (
455
+ <ContextualHelperPanel
456
+ hints={paintRegionHints(paintRegionMode, paintRegionNotice, paintRegionHovering)}
457
+ snapContext={snapContext}
458
+ title={paintRegionHudTitle(paintRegionMode)}
459
+ />
460
+ )
461
+ }
462
+
463
+ // The terrace tool draws like "Add mezzanine": same outline gestures and chip.
464
+ if (terraceShape) {
465
+ return (
466
+ <ContextualHelperPanel
467
+ chipHints={TERRACE_CHIP_HINTS}
468
+ hints={terraceHints(terraceShape)}
469
+ snapContext={snapContext}
470
+ title={TERRACE_HUD_TITLE}
471
+ />
472
+ )
473
+ }
474
+
475
+ if (openingShape) {
476
+ return (
477
+ <ContextualHelperPanel
478
+ hints={openingHints(openingShape)}
479
+ snapContext={snapContext}
480
+ title={OPENING_HUD_TITLE}
481
+ />
482
+ )
483
+ }
484
+
485
+ // "Add mezzanine" holds a handle-drag scope on its host room for the whole
486
+ // tool, so it must win over the generic resize HUD below.
487
+ if (mezzanineShape) {
488
+ return (
489
+ <ContextualHelperPanel
490
+ hints={mezzanineHints(mezzanineShape)}
491
+ snapContext={snapContext}
492
+ title={MEZZANINE_HUD_TITLE}
233
493
  />
234
494
  )
235
495
  }
@@ -246,6 +506,7 @@ export function HelperManager() {
246
506
  { keys: ['Esc'], label: 'Cancel' },
247
507
  ]}
248
508
  snapContext={snapContext}
509
+ title={RESIZE_HUD_TITLE}
249
510
  />
250
511
  )
251
512
  }
@@ -254,25 +515,54 @@ export function HelperManager() {
254
515
  // before the select branch so the idle "drag selected / add objects" hints
255
516
  // never leak over an in-progress reshape — and it gets its own snapping chip.
256
517
  if (scope.kind === 'reshaping') {
257
- return <ContextualHelperPanel hints={reshapingHints(scope.reshape)} snapContext={snapContext} />
518
+ // A kind's own reshape brings its hints (`def.affordanceHints[reshape]`).
519
+ const hints = reshapingNode
520
+ ? nodeRegistry.get(reshapingNode.type)?.affordanceHints?.[scope.reshape]
521
+ : undefined
522
+ const title = reshapingNode
523
+ ? nodeKindHudTitle(reshapingNode.type, scope.reshape === 'split' ? 'Split' : 'Edit')
524
+ : null
525
+ if (hints) {
526
+ return (
527
+ <RegisteredToolHelper
528
+ hints={hints}
529
+ shiftPressed={modifiers.shift}
530
+ snapContext={snapContext}
531
+ title={title}
532
+ />
533
+ )
534
+ }
535
+ return (
536
+ <ContextualHelperPanel
537
+ hints={reshapingHints(scope.reshape)}
538
+ snapContext={snapContext}
539
+ title={title}
540
+ />
541
+ )
258
542
  }
259
543
 
260
544
  if (movingNode) {
261
- if (movingNode.type === 'building') return <BuildingHelper showRotate />
262
545
  // A fresh placement (e.g. a positioned preset like a shelf) advertises its
263
546
  // once/repeat continuation, exactly like the GLB item tool — but an existing
264
547
  // node being *moved* is not a placement, so it gets no continuation chip.
265
- const movingContinuationContext = isFreshPlacementMetadata(movingNode.metadata)
266
- ? continuationContextOf(movingNode.type)
548
+ const isFreshPlacement = isFreshPlacementMetadata(movingNode.metadata)
549
+ const movingTitle = nodeKindHudTitle(movingNode.type, isFreshPlacement ? undefined : 'Move')
550
+ if (movingNode.type === 'building') return <BuildingHelper showRotate title={movingTitle} />
551
+ const movingContinuationContext = isFreshPlacement
552
+ ? keyCyclableContinuationContext(continuationContextOf(movingNode.type))
267
553
  : null
268
- const collisionValidatesDrop =
269
- nodeRegistry.get(movingNode.type)?.capabilities.floorPlaced?.collides === true
554
+ const collisionValidatesDrop = floorPlacedCollides(
555
+ nodeRegistry.get(movingNode.type)?.capabilities.floorPlaced,
556
+ movingNode,
557
+ )
270
558
  return (
271
559
  <ItemHelper
272
560
  continuationContext={movingContinuationContext}
561
+ notice={placementNotice?.line}
273
562
  showEsc
274
563
  showForce={collisionValidatesDrop}
275
564
  snapContext={snapContext}
565
+ title={movingTitle}
276
566
  />
277
567
  )
278
568
  }
@@ -281,7 +571,19 @@ export function HelperManager() {
281
571
  // only contextual control here. The chip hides itself for targets that only
282
572
  // paint one surface, so this renders nothing until a scoped target is active.
283
573
  if (mode === 'material-paint') {
284
- return <ContextualHelperPanel hints={[]} showPaintScope />
574
+ return (
575
+ <ContextualHelperPanel
576
+ hints={paintHints(paintRegionMode)}
577
+ showPaintScope
578
+ title={
579
+ paintRegionMode === 'pick'
580
+ ? PICK_MATERIAL_HUD_TITLE
581
+ : paintRegionMode === 'erase'
582
+ ? ERASE_HUD_TITLE
583
+ : PAINT_HUD_TITLE
584
+ }
585
+ />
586
+ )
285
587
  }
286
588
 
287
589
  // Sculpt mode. The HUD is what makes a sustained brush mode legible: it names
@@ -290,17 +592,41 @@ export function HelperManager() {
290
592
  // mode-specific — bracket resize, and an Esc that abandons the stroke rather
291
593
  // than exiting the mode.
292
594
  if (mode === 'terrain-sculpt') {
293
- return <ContextualHelperPanel hints={terrainSculptHints(terrainVerb, terrainSampling)} />
595
+ return (
596
+ <ContextualHelperPanel
597
+ hints={terrainSculptHints(terrainVerb, terrainSampling)}
598
+ title={terrainHudTitle(terrainVerb)}
599
+ />
600
+ )
294
601
  }
295
602
 
296
603
  if (scope.kind === 'mesh-editing') {
297
- return <ContextualHelperPanel hints={contextualEditHints} snapContext={snapContext} />
604
+ return (
605
+ <ContextualHelperPanel
606
+ hints={contextualEditHints}
607
+ snapContext={snapContext}
608
+ title={contextualHelpNode ? nodeKindHudTitle(contextualHelpNode.type, 'Edit') : null}
609
+ />
610
+ )
611
+ }
612
+
613
+
614
+ // Divide draws a separator path across the selected room — it reads as a
615
+ // wall draft: the wall's snapping chips (Shift / Ctrl) plus the path keys.
616
+ if (scope.kind === 'room-divide') {
617
+ return (
618
+ <ContextualHelperPanel
619
+ hints={ROOM_DIVIDE_HINTS}
620
+ snapContext={snapContext}
621
+ title={DIVIDE_ROOM_HUD_TITLE}
622
+ />
623
+ )
298
624
  }
299
625
 
300
626
  // Idle select only — an active scope (handle-drag, box-select, …) must not show
301
627
  // the idle selection hints.
302
628
  if (mode === 'select' && scope.kind === 'idle') {
303
- return <ContextualHelperPanel hints={selectModeHints} />
629
+ return <ContextualHelperPanel hints={selectModeHints} title={SELECT_HUD_TITLE} />
304
630
  }
305
631
 
306
632
  if (tool === 'measurement' && measurementToolKind === 'smart') {
@@ -311,6 +637,7 @@ export function HelperManager() {
311
637
  { keys: ['Click'], label: 'Pin measurement lens' },
312
638
  { keys: ['Esc'], label: 'Exit smart measure' },
313
639
  ]}
640
+ title={SMART_MEASURE_HUD_TITLE}
314
641
  />
315
642
  )
316
643
  }
@@ -328,8 +655,10 @@ export function HelperManager() {
328
655
  <RegisteredToolHelper
329
656
  continuationContext={continuationContext}
330
657
  hints={hints}
658
+ notice={placementNotice?.line ?? null}
331
659
  shiftPressed={modifiers.shift}
332
660
  snapContext={snapContext}
661
+ title={toolHudTitle(tool, wallMode)}
333
662
  />
334
663
  )
335
664
  }
@@ -1,4 +1,5 @@
1
1
  import type { ContinuationContext } from '../../../lib/continuation'
2
+ import type { HudTitle } from '../../../lib/hud-title'
2
3
  import type { SnapContext } from '../../../lib/snapping-mode'
3
4
  import { ContextualHelperPanel } from './contextual-helper-panel'
4
5
 
@@ -11,6 +12,9 @@ interface ItemHelperProps {
11
12
  // Set for a fresh point-kind placement (e.g. a positioned preset) so the
12
13
  // once/repeat continuation chip shows; null for an existing-node move.
13
14
  continuationContext?: ContinuationContext | null
15
+ title?: HudTitle | null
16
+ // Why the item would not fit where it is (a door's way, a room too small): a warning, not a block.
17
+ notice?: string | null
14
18
  }
15
19
 
16
20
  // Snapping mode is the chip on the right (Shift cycles it), so it's not repeated
@@ -20,10 +24,13 @@ export function ItemHelper({
20
24
  snapContext,
21
25
  showForce,
22
26
  continuationContext = null,
27
+ title = null,
28
+ notice = null,
23
29
  }: ItemHelperProps) {
24
30
  return (
25
31
  <ContextualHelperPanel
26
32
  continuationContext={continuationContext}
33
+ notice={notice}
27
34
  hints={[
28
35
  { keys: ['Left click'], label: 'Place' },
29
36
  { keys: ['R', 'T'], label: 'Rotate' },
@@ -31,6 +38,7 @@ export function ItemHelper({
31
38
  { keys: [showEsc ? 'Esc' : 'Right click'], label: 'Cancel' },
32
39
  ]}
33
40
  snapContext={snapContext}
41
+ title={title}
34
42
  />
35
43
  )
36
44
  }
@@ -1,8 +1,8 @@
1
1
  import type { ToolHint } from '@pascal-app/core'
2
- import { useMemo, useSyncExternalStore } from 'react'
3
2
  import type { ContinuationContext } from '../../../lib/continuation'
3
+ import type { HudTitle } from '../../../lib/hud-title'
4
4
  import type { SnapContext } from '../../../lib/snapping-mode'
5
- import useEditor from '../../../store/use-editor'
5
+ import { useVisibleToolHints } from '../../../lib/panel-tool-options'
6
6
  import { ContextualHelperPanel } from './contextual-helper-panel'
7
7
 
8
8
  /**
@@ -19,43 +19,18 @@ export function RegisteredToolHelper({
19
19
  shiftPressed = false,
20
20
  snapContext = null,
21
21
  continuationContext = null,
22
+ title = null,
23
+ notice = null,
22
24
  }: {
23
25
  hints: ToolHint[]
24
26
  shiftPressed?: boolean
25
27
  snapContext?: SnapContext | null
26
28
  continuationContext?: ContinuationContext | null
29
+ title?: HudTitle | null
30
+ notice?: string | null
27
31
  }) {
28
- // Live vertex count of an in-progress polygon draft, so hints gated on a
29
- // minimum (e.g. "Finish" at ≥ 3) only appear once they're actually possible.
30
- const draftVertexCount = useEditor((s) => s.draftVertexCount)
31
- const visibilityStore = useMemo(
32
- () => ({
33
- subscribe: (onChange: () => void) => {
34
- const unsubscribers = hints.flatMap((hint) =>
35
- hint.visible ? [hint.visible.subscribe(onChange)] : [],
36
- )
37
- return () => {
38
- for (const unsubscribe of unsubscribers) unsubscribe()
39
- }
40
- },
41
- getSnapshot: () => hints.map((hint) => (hint.visible?.value() === false ? '0' : '1')).join(''),
42
- }),
43
- [hints],
44
- )
45
- useSyncExternalStore(
46
- visibilityStore.subscribe,
47
- visibilityStore.getSnapshot,
48
- visibilityStore.getSnapshot,
49
- )
50
- // Some hints are replaced by live contextual chips, so keep the generic
51
- // registry renderer from duplicating stale/static versions.
52
- const visible = hints.filter(
53
- (hint) =>
54
- !(hint.key === 'Shift' && hint.label === 'Cycle snapping mode') &&
55
- hint.visible?.value() !== false &&
56
- (hint.minDraftVertices == null || draftVertexCount >= hint.minDraftVertices),
57
- )
58
- if (visible.length === 0 && !snapContext && !continuationContext) return null
32
+ const visible = useVisibleToolHints(hints)
33
+ if (visible.length === 0 && !snapContext && !continuationContext && !notice) return null
59
34
  // Hints carrying a live-state `chip` render as mode chips next to the
60
35
  // snapping / continuation rows; the rest stay static key rows.
61
36
  const chipHints = visible.filter((hint) => hint.chip)
@@ -74,7 +49,9 @@ export function RegisteredToolHelper({
74
49
  }
75
50
  })}
76
51
  continuationContext={continuationContext}
52
+ notice={notice}
77
53
  snapContext={snapContext}
54
+ title={title}
78
55
  />
79
56
  )
80
57
  }