@pascal-app/editor 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (440) hide show
  1. package/package.json +11 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +202 -57
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +36 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -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,10 +21,50 @@ 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, {
@@ -33,6 +74,7 @@ import useInteractionScope, {
33
74
  } from '../../../store/use-interaction-scope'
34
75
  import { BuildingHelper } from './building-helper'
35
76
  import { ContextualHelperPanel } from './contextual-helper-panel'
77
+ import { transientDraftId, usePlacementNotice } from '../../../hooks/use-placement-notice'
36
78
  import { ItemHelper } from './item-helper'
37
79
  import { RegisteredToolHelper } from './registered-tool-helper'
38
80
 
@@ -91,6 +133,89 @@ type ActiveModifierKeys = {
91
133
  }
92
134
 
93
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
+ ]
94
219
  const NO_CONTEXTUAL_HELP_SUBSCRIPTION = () => () => {}
95
220
 
96
221
  function useActiveModifierKeys(): ActiveModifierKeys {
@@ -129,6 +254,43 @@ function useActiveModifierKeys(): ActiveModifierKeys {
129
254
  return modifiers
130
255
  }
131
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
+
132
294
  export function HelperManager() {
133
295
  const mode = useEditor((s) => s.mode)
134
296
  const tool = useEditor((s) => s.tool)
@@ -136,11 +298,24 @@ export function HelperManager() {
136
298
  const terrainSampling = useEditor((s) => s.terrainSampling)
137
299
  const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
138
300
  const measurementToolKind = useEditor((s) => s.toolDefaults.measurement?.kind)
301
+ const wallMode = useEditor((s) => s.continuationByContext.wall)
139
302
  const workspaceMode = useEditor((s) => s.workspaceMode)
140
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))
141
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)
142
316
  const reshapingNode = useReshapingNode()
143
317
  const activeHandleDrag = useActiveHandleDrag()
318
+ const mezzanineHandle = useScene((s) => mezzanineGesture(activeHandleDrag, s.nodes))
144
319
  const selectedIds = useViewer((s) => s.selection.selectedIds)
145
320
  const isMobile = useIsMobile()
146
321
  const modifiers = useActiveModifierKeys()
@@ -180,11 +355,14 @@ export function HelperManager() {
180
355
  return node ? nodeRegistry.get(node.type)?.snapProfile : undefined
181
356
  },
182
357
  draftDirectionalOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapDraftDirectional ?? true,
358
+ paintRegion: mode === 'material-paint' && paintRegionDrawing,
183
359
  }),
184
- [scope, mode, tool],
360
+ [scope, mode, tool, paintRegionDrawing],
185
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.
186
364
  const continuationContext = useMemo(
187
- () => getActiveContinuationContext(),
365
+ () => keyCyclableContinuationContext(getActiveContinuationContext()),
188
366
  [scope, mode, tool],
189
367
  )
190
368
  const selectModeHints = useMemo(() => {
@@ -226,6 +404,7 @@ export function HelperManager() {
226
404
  <ContextualHelperPanel
227
405
  hints={resolveRotateHandleHelpHints(modifiers.alt)}
228
406
  snapContext={snapContext}
407
+ title={ROTATE_HUD_TITLE}
229
408
  />
230
409
  )
231
410
  }
@@ -238,6 +417,79 @@ export function HelperManager() {
238
417
  <ContextualHelperPanel
239
418
  hints={[{ keys: ['R / T'], label: 'Rotate the selection ±45°' }]}
240
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}
241
493
  />
242
494
  )
243
495
  }
@@ -254,6 +506,7 @@ export function HelperManager() {
254
506
  { keys: ['Esc'], label: 'Cancel' },
255
507
  ]}
256
508
  snapContext={snapContext}
509
+ title={RESIZE_HUD_TITLE}
257
510
  />
258
511
  )
259
512
  }
@@ -266,30 +519,50 @@ export function HelperManager() {
266
519
  const hints = reshapingNode
267
520
  ? nodeRegistry.get(reshapingNode.type)?.affordanceHints?.[scope.reshape]
268
521
  : undefined
522
+ const title = reshapingNode
523
+ ? nodeKindHudTitle(reshapingNode.type, scope.reshape === 'split' ? 'Split' : 'Edit')
524
+ : null
269
525
  if (hints) {
270
526
  return (
271
- <RegisteredToolHelper hints={hints} shiftPressed={modifiers.shift} snapContext={snapContext} />
527
+ <RegisteredToolHelper
528
+ hints={hints}
529
+ shiftPressed={modifiers.shift}
530
+ snapContext={snapContext}
531
+ title={title}
532
+ />
272
533
  )
273
534
  }
274
- return <ContextualHelperPanel hints={reshapingHints(scope.reshape)} snapContext={snapContext} />
535
+ return (
536
+ <ContextualHelperPanel
537
+ hints={reshapingHints(scope.reshape)}
538
+ snapContext={snapContext}
539
+ title={title}
540
+ />
541
+ )
275
542
  }
276
543
 
277
544
  if (movingNode) {
278
- if (movingNode.type === 'building') return <BuildingHelper showRotate />
279
545
  // A fresh placement (e.g. a positioned preset like a shelf) advertises its
280
546
  // once/repeat continuation, exactly like the GLB item tool — but an existing
281
547
  // node being *moved* is not a placement, so it gets no continuation chip.
282
- const movingContinuationContext = isFreshPlacementMetadata(movingNode.metadata)
283
- ? 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))
284
553
  : null
285
- const collisionValidatesDrop =
286
- nodeRegistry.get(movingNode.type)?.capabilities.floorPlaced?.collides === true
554
+ const collisionValidatesDrop = floorPlacedCollides(
555
+ nodeRegistry.get(movingNode.type)?.capabilities.floorPlaced,
556
+ movingNode,
557
+ )
287
558
  return (
288
559
  <ItemHelper
289
560
  continuationContext={movingContinuationContext}
561
+ notice={placementNotice?.line}
290
562
  showEsc
291
563
  showForce={collisionValidatesDrop}
292
564
  snapContext={snapContext}
565
+ title={movingTitle}
293
566
  />
294
567
  )
295
568
  }
@@ -298,7 +571,19 @@ export function HelperManager() {
298
571
  // only contextual control here. The chip hides itself for targets that only
299
572
  // paint one surface, so this renders nothing until a scoped target is active.
300
573
  if (mode === 'material-paint') {
301
- 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
+ )
302
587
  }
303
588
 
304
589
  // Sculpt mode. The HUD is what makes a sustained brush mode legible: it names
@@ -307,18 +592,41 @@ export function HelperManager() {
307
592
  // mode-specific — bracket resize, and an Esc that abandons the stroke rather
308
593
  // than exiting the mode.
309
594
  if (mode === 'terrain-sculpt') {
310
- return <ContextualHelperPanel hints={terrainSculptHints(terrainVerb, terrainSampling)} />
595
+ return (
596
+ <ContextualHelperPanel
597
+ hints={terrainSculptHints(terrainVerb, terrainSampling)}
598
+ title={terrainHudTitle(terrainVerb)}
599
+ />
600
+ )
311
601
  }
312
602
 
313
603
  if (scope.kind === 'mesh-editing') {
314
- 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
+ )
315
611
  }
316
612
 
317
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
+ )
624
+ }
625
+
318
626
  // Idle select only — an active scope (handle-drag, box-select, …) must not show
319
627
  // the idle selection hints.
320
628
  if (mode === 'select' && scope.kind === 'idle') {
321
- return <ContextualHelperPanel hints={selectModeHints} />
629
+ return <ContextualHelperPanel hints={selectModeHints} title={SELECT_HUD_TITLE} />
322
630
  }
323
631
 
324
632
  if (tool === 'measurement' && measurementToolKind === 'smart') {
@@ -329,6 +637,7 @@ export function HelperManager() {
329
637
  { keys: ['Click'], label: 'Pin measurement lens' },
330
638
  { keys: ['Esc'], label: 'Exit smart measure' },
331
639
  ]}
640
+ title={SMART_MEASURE_HUD_TITLE}
332
641
  />
333
642
  )
334
643
  }
@@ -346,8 +655,10 @@ export function HelperManager() {
346
655
  <RegisteredToolHelper
347
656
  continuationContext={continuationContext}
348
657
  hints={hints}
658
+ notice={placementNotice?.line ?? null}
349
659
  shiftPressed={modifiers.shift}
350
660
  snapContext={snapContext}
661
+ title={toolHudTitle(tool, wallMode)}
351
662
  />
352
663
  )
353
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,5 +1,6 @@
1
1
  import type { ToolHint } from '@pascal-app/core'
2
2
  import type { ContinuationContext } from '../../../lib/continuation'
3
+ import type { HudTitle } from '../../../lib/hud-title'
3
4
  import type { SnapContext } from '../../../lib/snapping-mode'
4
5
  import { useVisibleToolHints } from '../../../lib/panel-tool-options'
5
6
  import { ContextualHelperPanel } from './contextual-helper-panel'
@@ -18,14 +19,18 @@ export function RegisteredToolHelper({
18
19
  shiftPressed = false,
19
20
  snapContext = null,
20
21
  continuationContext = null,
22
+ title = null,
23
+ notice = null,
21
24
  }: {
22
25
  hints: ToolHint[]
23
26
  shiftPressed?: boolean
24
27
  snapContext?: SnapContext | null
25
28
  continuationContext?: ContinuationContext | null
29
+ title?: HudTitle | null
30
+ notice?: string | null
26
31
  }) {
27
32
  const visible = useVisibleToolHints(hints)
28
- if (visible.length === 0 && !snapContext && !continuationContext) return null
33
+ if (visible.length === 0 && !snapContext && !continuationContext && !notice) return null
29
34
  // Hints carrying a live-state `chip` render as mode chips next to the
30
35
  // snapping / continuation rows; the rest stay static key rows.
31
36
  const chipHints = visible.filter((hint) => hint.chip)
@@ -44,7 +49,9 @@ export function RegisteredToolHelper({
44
49
  }
45
50
  })}
46
51
  continuationContext={continuationContext}
52
+ notice={notice}
47
53
  snapContext={snapContext}
54
+ title={title}
48
55
  />
49
56
  )
50
57
  }
@@ -3,6 +3,50 @@
3
3
  import { Icon } from '@iconify/react'
4
4
  import type { IconRef } from '@pascal-app/core'
5
5
  import { type ComponentType, lazy, Suspense } from 'react'
6
+ import { cn } from '../../lib/utils'
7
+
8
+ const SVG_DATA_URI = /^data:image\/svg\+xml[^,]*,/
9
+
10
+ /** Whether a `data:image/svg+xml` URI's markup paints with `currentColor`. */
11
+ function isCurrentColorSvg(src: string): boolean {
12
+ const head = SVG_DATA_URI.exec(src)
13
+ if (!head) return false
14
+ const body = src.slice(head[0].length)
15
+ try {
16
+ const markup = head[0].includes(';base64') ? atob(body) : decodeURIComponent(body)
17
+ return markup.includes('currentColor')
18
+ } catch {
19
+ return false
20
+ }
21
+ }
22
+
23
+ /**
24
+ * A `url`-kind icon. An SVG behind an `<img>` renders in its own document, so
25
+ * `currentColor` in the markup resolves to black rather than the surrounding
26
+ * text colour — a monochrome plugin glyph then disappears against the dark
27
+ * sidebar. Such an SVG becomes a CSS mask over the text colour instead. Plugin
28
+ * manifests are third-party, so the markup is never inlined into the page.
29
+ */
30
+ export function IconRefImage({
31
+ className,
32
+ size,
33
+ src,
34
+ }: {
35
+ className?: string
36
+ size?: number
37
+ src: string
38
+ }) {
39
+ const style = size === undefined ? undefined : { height: size, width: size }
40
+ if (!isCurrentColorSvg(src)) return <img alt="" className={className} src={src} style={style} />
41
+ const mask = `url("${src.replace(/["\\\n\r]/g, encodeURIComponent)}") center / contain no-repeat`
42
+ return (
43
+ <span
44
+ aria-hidden
45
+ className={cn('inline-block bg-current', className)}
46
+ style={{ ...style, mask, WebkitMask: mask }}
47
+ />
48
+ )
49
+ }
6
50
 
7
51
  // `React.lazy` must be called once per loader so the resolved component keeps
8
52
  // a stable identity across renders (otherwise every parent re-render remounts
@@ -26,7 +70,7 @@ function resolveLazyIcon(module: () => Promise<{ default: ComponentType }>): Com
26
70
  */
27
71
  export function IconRefGlyph({ icon, size = 16 }: { icon: IconRef; size?: number }) {
28
72
  if (icon.kind === 'url') {
29
- return <img alt="" className="shrink-0 object-contain" height={size} src={icon.src} width={size} />
73
+ return <IconRefImage className="shrink-0" size={size} src={icon.src} />
30
74
  }
31
75
  if (icon.kind === 'iconify') {
32
76
  return <Icon height={size} icon={icon.name} width={size} />
@@ -2,7 +2,7 @@
2
2
 
3
3
  import type { LevelNode } from '@pascal-app/core'
4
4
  import { useEffect, useState } from 'react'
5
- import type { LevelDuplicatePreset } from '../../lib/level-duplication'
5
+ import type { LevelDuplicatePreset } from '@pascal-app/core/building'
6
6
  import { getLevelDisplayName } from '@pascal-app/core'
7
7
  import { cn } from '../../lib/utils'
8
8
  import {
@@ -0,0 +1,16 @@
1
+ 'use client'
2
+
3
+ import { ARTIFACT_URL_PREFIX, resolveArtifactUrl } from '@pascal-app/core'
4
+ import Image, { type ImageProps } from 'next/image'
5
+
6
+ /**
7
+ * A node's icon or thumbnail. A scripted object's thumbnail is an
8
+ * `artifact://` image: it resolves through the project's artifact store and
9
+ * skips image optimization, whose server-side fetch has no session to read a
10
+ * private project's artifact with.
11
+ */
12
+ export function NodeIconImage({ src, ...props }: Omit<ImageProps, 'src'> & { src: string }) {
13
+ if (!src.startsWith(ARTIFACT_URL_PREFIX)) return <Image src={src} {...props} />
14
+ const url = resolveArtifactUrl(src)
15
+ return url ? <Image src={url} unoptimized {...props} /> : null
16
+ }