@pascal-app/editor 1.0.6 → 1.0.7

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 (317) hide show
  1. package/package.json +18 -15
  2. package/src/components/editor/action-menu-button.tsx +4 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
  4. package/src/components/editor/bake-thumbnail.tsx +3 -2
  5. package/src/components/editor/custom-camera-controls.tsx +20 -8
  6. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  7. package/src/components/editor/first-person-controls.tsx +72 -61
  8. package/src/components/editor/floating-action-menu.tsx +27 -9
  9. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  10. package/src/components/editor/floorplan-panel.test.ts +220 -0
  11. package/src/components/editor/floorplan-panel.tsx +285 -132
  12. package/src/components/editor/footprint-height-handle.tsx +1 -1
  13. package/src/components/editor/grid.tsx +24 -15
  14. package/src/components/editor/group-actions.ts +19 -1
  15. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  16. package/src/components/editor/group-move-3d.ts +27 -2
  17. package/src/components/editor/group-selection-box-3d.tsx +4 -1
  18. package/src/components/editor/group-transform-shared.ts +20 -0
  19. package/src/components/editor/index.tsx +130 -20
  20. package/src/components/editor/inspector-extension-actions.tsx +36 -0
  21. package/src/components/editor/localized-html.tsx +22 -0
  22. package/src/components/editor/node-action-menu.tsx +46 -12
  23. package/src/components/editor/node-arrow-handles.tsx +1 -2
  24. package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
  25. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  26. package/src/components/editor/registry-action-contributions.test.tsx +67 -6
  27. package/src/components/editor/registry-action-contributions.tsx +115 -15
  28. package/src/components/editor/riser-diagram-panel.tsx +59 -9
  29. package/src/components/editor/room-controls.tsx +1 -1
  30. package/src/components/editor/room-handles-2d.tsx +156 -6
  31. package/src/components/editor/room-handles.tsx +241 -51
  32. package/src/components/editor/room-highlight.tsx +2 -0
  33. package/src/components/editor/room-labels.tsx +29 -15
  34. package/src/components/editor/selection-manager.tsx +15 -102
  35. package/src/components/editor/separator-end-grip.tsx +74 -0
  36. package/src/components/editor/site-edge-labels.tsx +1 -1
  37. package/src/components/editor/thumbnail-generator.test.ts +15 -6
  38. package/src/components/editor/thumbnail-generator.tsx +26 -1
  39. package/src/components/editor/use-floorplan-background-placement.ts +55 -52
  40. package/src/components/editor/views/view-bar.tsx +111 -26
  41. package/src/components/editor/wall-measurement-label.tsx +1 -1
  42. package/src/components/editor/wall-move-side-handles.tsx +5 -1
  43. package/src/components/editor/wall-pull-grip.tsx +193 -0
  44. package/src/components/editor/wall-snap-beacon-layer.tsx +81 -1
  45. package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
  46. package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
  47. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
  48. package/src/components/editor-2d/floorplan-group-move.tsx +31 -3
  49. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
  50. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +22 -13
  51. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +11 -3
  52. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +7 -1
  53. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
  54. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
  55. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +12 -1
  56. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +197 -37
  57. package/src/components/procedural-items/inspector.tsx +59 -12
  58. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
  59. package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
  60. package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
  61. package/src/components/tools/distribution/distribution-run-tool.tsx +50 -26
  62. package/src/components/tools/item/placement-history.test.tsx +1 -1
  63. package/src/components/tools/item/placement-strategies.test.ts +64 -1
  64. package/src/components/tools/item/placement-strategies.ts +5 -9
  65. package/src/components/tools/item/use-placement-coordinator.tsx +30 -34
  66. package/src/components/tools/registry/item-surface-move.test.ts +11 -0
  67. package/src/components/tools/select/select-candidates.test.ts +45 -0
  68. package/src/components/tools/select/select-candidates.ts +13 -2
  69. package/src/components/tools/shared/cursor-sphere.tsx +10 -3
  70. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  71. package/src/components/tools/shared/placement-box.tsx +1 -1
  72. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  73. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  74. package/src/components/tools/wall/wall-drafting.test.ts +299 -23
  75. package/src/components/tools/wall/wall-drafting.ts +132 -24
  76. package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
  77. package/src/components/tools/wall/wall-pull-snap.ts +563 -0
  78. package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
  79. package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
  80. package/src/components/tools/zone/zone-tool.tsx +3 -3
  81. package/src/components/ui/action-menu/action-button.tsx +2 -0
  82. package/src/components/ui/action-menu/camera-actions.tsx +8 -7
  83. package/src/components/ui/action-menu/control-modes.tsx +9 -5
  84. package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
  85. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
  86. package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
  87. package/src/components/ui/action-menu/index.tsx +1 -0
  88. package/src/components/ui/action-menu/measurement-control.tsx +35 -29
  89. package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
  90. package/src/components/ui/action-menu/structure-tools.tsx +25 -23
  91. package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
  92. package/src/components/ui/action-menu/view-toggles.tsx +256 -51
  93. package/src/components/ui/build-panel/build-panel.tsx +163 -25
  94. package/src/components/ui/command-palette/editor-commands.tsx +38 -11
  95. package/src/components/ui/controls/finish-swatch.tsx +45 -19
  96. package/src/components/ui/controls/material-picker.tsx +24 -17
  97. package/src/components/ui/controls/metric-control.test.ts +52 -0
  98. package/src/components/ui/controls/metric-control.tsx +62 -24
  99. package/src/components/ui/controls/segmented-control.tsx +5 -2
  100. package/src/components/ui/controls/slider-control.test.tsx +23 -0
  101. package/src/components/ui/controls/slider-control.tsx +30 -14
  102. package/src/components/ui/floating-level-selector.tsx +100 -48
  103. package/src/components/ui/helpers/building-helper.tsx +5 -3
  104. package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
  105. package/src/components/ui/helpers/helper-manager.test.tsx +53 -3
  106. package/src/components/ui/helpers/helper-manager.tsx +82 -57
  107. package/src/components/ui/helpers/item-helper.tsx +6 -4
  108. package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
  109. package/src/components/ui/level-duplicate-dialog.tsx +32 -23
  110. package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
  111. package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
  112. package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
  113. package/src/components/ui/panels/floor-foundation-panel.tsx +113 -38
  114. package/src/components/ui/panels/floor-opening-panel.tsx +73 -25
  115. package/src/components/ui/panels/inspector-extensions.tsx +66 -0
  116. package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
  117. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -6
  118. package/src/components/ui/panels/multi-field-value.test.ts +27 -0
  119. package/src/components/ui/panels/multi-field-value.ts +20 -1
  120. package/src/components/ui/panels/multi-height-mode.tsx +8 -7
  121. package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
  122. package/src/components/ui/panels/multi-selection-panel.tsx +75 -35
  123. package/src/components/ui/panels/node-display.ts +35 -24
  124. package/src/components/ui/panels/panel-manager.tsx +5 -3
  125. package/src/components/ui/panels/panel-wrapper.tsx +22 -109
  126. package/src/components/ui/panels/parametric-field-control.tsx +13 -10
  127. package/src/components/ui/panels/parametric-inspector.tsx +18 -12
  128. package/src/components/ui/panels/reference-panel.tsx +51 -35
  129. package/src/components/ui/panels/room-construction-rows.tsx +464 -65
  130. package/src/components/ui/panels/room-name-options.test.ts +36 -3
  131. package/src/components/ui/panels/room-name-options.ts +110 -11
  132. package/src/components/ui/panels/room-openings-field.tsx +21 -12
  133. package/src/components/ui/panels/room-panel.tsx +206 -99
  134. package/src/components/ui/panels/shape-choice.tsx +7 -5
  135. package/src/components/ui/panels/wall-region-list.tsx +7 -7
  136. package/src/components/ui/primitives/shortcut-token.tsx +10 -6
  137. package/src/components/ui/sidebar/icon-rail.tsx +11 -9
  138. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
  139. package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
  140. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
  141. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
  142. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -2
  144. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
  145. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
  146. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
  147. package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
  148. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
  149. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
  150. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
  151. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
  152. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
  153. package/src/components/ui/sidebar/panels/site-panel/index.tsx +117 -64
  154. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
  155. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
  156. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
  157. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
  158. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
  159. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
  160. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +8 -3
  161. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
  162. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
  163. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
  164. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
  165. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
  166. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
  167. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
  168. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
  169. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
  170. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +18 -8
  171. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
  172. package/src/components/ui/sidebar/tab-bar.tsx +4 -1
  173. package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
  174. package/src/components/ui/snap-target-badge.tsx +7 -6
  175. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
  176. package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
  177. package/src/components/viewer/floorplan-compass-button.tsx +12 -3
  178. package/src/components/viewer/floorplan-preview.tsx +27 -19
  179. package/src/components/viewer/render-settings-menu.tsx +294 -0
  180. package/src/components/viewer/sky-control.tsx +710 -0
  181. package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
  182. package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
  183. package/src/components/viewer/viewer-controls-bar.tsx +278 -260
  184. package/src/components/viewer/viewer-hover-label.tsx +12 -4
  185. package/src/components/viewer/viewer-rooms.tsx +61 -4
  186. package/src/components/viewer/viewer-scene-header.tsx +28 -17
  187. package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
  188. package/src/components/viewer-overlay.tsx +8 -0
  189. package/src/components/walkthrough-hud.tsx +75 -2
  190. package/src/hooks/use-keyboard.test.ts +85 -4
  191. package/src/hooks/use-keyboard.ts +65 -7
  192. package/src/hooks/use-selected-room.ts +12 -1
  193. package/src/i18n/floor-footprint-name.ts +30 -0
  194. package/src/i18n/level-name.ts +39 -0
  195. package/src/i18n/locales/en/messages.po +15117 -0
  196. package/src/i18n/locales/en/messages.ts +1 -0
  197. package/src/i18n/locales/en-XA/messages.ts +1 -0
  198. package/src/i18n/locales/es/messages.po +37501 -0
  199. package/src/i18n/locales/es/messages.ts +1 -0
  200. package/src/i18n/locales/fr/messages.po +37501 -0
  201. package/src/i18n/locales/fr/messages.ts +1 -0
  202. package/src/i18n/locales/ja/messages.po +37501 -0
  203. package/src/i18n/locales/ja/messages.ts +1 -0
  204. package/src/i18n/locales/zh-CN/messages.po +37501 -0
  205. package/src/i18n/locales/zh-CN/messages.ts +1 -0
  206. package/src/i18n/locales.test.ts +63 -0
  207. package/src/i18n/locales.ts +32 -0
  208. package/src/i18n/message-slot.tsx +25 -0
  209. package/src/i18n/node-messages.ts +169 -0
  210. package/src/i18n/pseudo-locale.ts +17 -0
  211. package/src/i18n/published-entry.test.ts +17 -0
  212. package/src/i18n/scene-theme-name.ts +16 -0
  213. package/src/index.tsx +156 -4
  214. package/src/lib/add-storey.test.ts +3 -3
  215. package/src/lib/contextual-help.test.ts +20 -1
  216. package/src/lib/contextual-help.ts +95 -19
  217. package/src/lib/continuation.ts +38 -11
  218. package/src/lib/floor-footprints.test.ts +2 -1
  219. package/src/lib/floor-foundation-support.test.ts +7 -3
  220. package/src/lib/floorplan/dimension-drive.test.ts +36 -2
  221. package/src/lib/floorplan/dimension-drive.ts +68 -3
  222. package/src/lib/floorplan/floorplan-export.test.ts +16 -5
  223. package/src/lib/floorplan/floorplan-export.tsx +13 -16
  224. package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
  225. package/src/lib/floorplan/floorplan-extension.ts +37 -18
  226. package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
  227. package/src/lib/floorplan/floorplan-mode.ts +14 -12
  228. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
  229. package/src/lib/floorplan/floorplan-readonly.ts +3 -3
  230. package/src/lib/floorplan/schedules.ts +10 -6
  231. package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
  232. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
  233. package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
  234. package/src/lib/geometry-script/author.test.ts +114 -0
  235. package/src/lib/geometry-script/author.ts +55 -23
  236. package/src/lib/geometry-script/client.ts +3 -0
  237. package/src/lib/geometry-script/finish-scripts.ts +7 -7
  238. package/src/lib/glb-export.test.ts +327 -0
  239. package/src/lib/glb-export.ts +70 -2
  240. package/src/lib/grid-frame.test.ts +56 -3
  241. package/src/lib/grid-frame.ts +28 -1
  242. package/src/lib/history.test.ts +2 -1
  243. package/src/lib/hud-title.ts +48 -24
  244. package/src/lib/ifc-export.test.ts +41 -1
  245. package/src/lib/inspector-card-mode.test.ts +3 -31
  246. package/src/lib/inspector-card-mode.ts +5 -10
  247. package/src/lib/inspector-expanded.test.ts +13 -0
  248. package/src/lib/inspector-expanded.ts +13 -1
  249. package/src/lib/keyboard-pan.test.ts +7 -0
  250. package/src/lib/keyboard-pan.ts +9 -2
  251. package/src/lib/layer-thickness.test.ts +22 -0
  252. package/src/lib/layer-thickness.ts +79 -0
  253. package/src/lib/lot/drop-in.test.ts +59 -1
  254. package/src/lib/lot/drop-in.ts +42 -50
  255. package/src/lib/material-swatch-style.ts +24 -0
  256. package/src/lib/measurement-parser.ts +7 -3
  257. package/src/lib/measurements.test.ts +17 -0
  258. package/src/lib/measurements.ts +20 -5
  259. package/src/lib/mezzanine-selection.test.ts +3 -1
  260. package/src/lib/orbit-near-plane.test.ts +12 -0
  261. package/src/lib/orbit-near-plane.ts +13 -0
  262. package/src/lib/paint-regions.test.ts +8 -5
  263. package/src/lib/paint-scope.test.ts +53 -0
  264. package/src/lib/paint-scope.ts +18 -4
  265. package/src/lib/panel-rows.ts +17 -8
  266. package/src/lib/reference-scale.ts +83 -0
  267. package/src/lib/room-built-on.test.ts +26 -12
  268. package/src/lib/room-built-on.ts +20 -2
  269. package/src/lib/room-construction.test.ts +28 -1
  270. package/src/lib/room-construction.ts +14 -0
  271. package/src/lib/room-controls-phase6.test.ts +140 -0
  272. package/src/lib/room-handle-drag.ts +557 -42
  273. package/src/lib/room-name-catalog.test.ts +8 -0
  274. package/src/lib/room-name-catalog.ts +8 -1
  275. package/src/lib/room-name-messages.test.ts +132 -0
  276. package/src/lib/room-name-messages.ts +147 -0
  277. package/src/lib/room-name-storage.test.ts +334 -0
  278. package/src/lib/room-push-dimensions.test.ts +37 -0
  279. package/src/lib/room-push-dimensions.ts +226 -2
  280. package/src/lib/room-selection.test.ts +23 -6
  281. package/src/lib/scene-clipboard.test.ts +70 -49
  282. package/src/lib/scene-clipboard.ts +9 -4
  283. package/src/lib/selection-cycle.ts +63 -0
  284. package/src/lib/separator-edit.test.ts +431 -0
  285. package/src/lib/separator-edit.ts +179 -0
  286. package/src/lib/separator-lines.ts +38 -0
  287. package/src/lib/sfx-bus.ts +27 -1
  288. package/src/lib/sfx-player.ts +16 -0
  289. package/src/lib/tool-cursor-icon.ts +5 -1
  290. package/src/lib/tool-shortcuts.ts +130 -0
  291. package/src/lib/viewer-selection.test.ts +28 -24
  292. package/src/lib/viewer-selection.ts +20 -5
  293. package/src/lib/wall-draw-variant.test.ts +7 -6
  294. package/src/lib/wall-draw-variant.ts +107 -6
  295. package/src/lib/wall-nudge.test.ts +44 -0
  296. package/src/lib/wall-nudge.ts +131 -0
  297. package/src/lib/wall-parallel-snap.test.ts +83 -0
  298. package/src/lib/wall-parallel-snap.ts +176 -0
  299. package/src/lib/wall-placement-pointer.test.ts +23 -0
  300. package/src/lib/wall-placement-pointer.ts +18 -0
  301. package/src/lib/wall-pull.test.ts +245 -0
  302. package/src/lib/wall-pull.ts +209 -0
  303. package/src/lib/wall-push-line-up.test.ts +132 -0
  304. package/src/lib/wall-region-list.ts +14 -5
  305. package/src/lib/wall-run-snap.test.ts +188 -0
  306. package/src/lib/wall-run-snap.ts +83 -0
  307. package/src/lib/wall-type.test.ts +125 -0
  308. package/src/lib/wall-type.ts +119 -0
  309. package/src/lib/window-interaction.test.ts +42 -0
  310. package/src/lib/window-interaction.ts +47 -5
  311. package/src/lib/world-grid-snap.test.ts +139 -2
  312. package/src/lib/world-grid-snap.ts +102 -105
  313. package/src/store/use-alignment-guides.ts +27 -1
  314. package/src/store/use-editor.tsx +48 -0
  315. package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
  316. package/src/store/use-floorplan-annotation-visibility.ts +47 -22
  317. package/src/store/use-wall-snap-indicator.ts +15 -2
@@ -15,7 +15,6 @@ import {
15
15
  type NodeEvent,
16
16
  nodeRegistry,
17
17
  type RoofEvent,
18
- type RoofNode,
19
18
  type RoofSegmentEvent,
20
19
  type RoofSegmentNode,
21
20
  resolveLevelId,
@@ -29,7 +28,13 @@ import {
29
28
  useScene,
30
29
  wallFaceScriptPaintRefusal,
31
30
  } from '@pascal-app/core'
32
- import { getRoofMaterialArray, registerMaterialCacheCleanup, useViewer } from '@pascal-app/viewer'
31
+ import {
32
+ markHighlight,
33
+ ROOF_HEEL_MATERIAL_INDEX,
34
+ registerMaterialCacheCleanup,
35
+ tintEmissive,
36
+ useViewer,
37
+ } from '@pascal-app/viewer'
33
38
  import { useThree } from '@react-three/fiber'
34
39
  import { useCallback, useEffect, useRef } from 'react'
35
40
  import { type BufferGeometry, Color, type Material, type Mesh, type Object3D, Vector3 } from 'three'
@@ -57,7 +62,6 @@ import {
57
62
  import { selectionEnabled } from '../../lib/interaction/scope'
58
63
  import {
59
64
  type ActivePaintMaterial,
60
- buildRoofSurfaceMaterialPatch,
61
65
  hasActivePaintMaterial,
62
66
  resolveActivePaintMaterialFromSelection,
63
67
  } from '../../lib/material-paint'
@@ -90,7 +94,6 @@ import {
90
94
  paintScopeRole,
91
95
  paintSurfaceLabel,
92
96
  resolvePaintScopeTargets,
93
- slotDisplayLabel,
94
97
  type WallPaintHit,
95
98
  } from '../../lib/paint-scope'
96
99
  import {
@@ -248,9 +251,10 @@ function resolveStairMaterialTarget(
248
251
 
249
252
  function resolveRoofMaterialTarget(
250
253
  event: RoofEvent | RoofSegmentEvent,
251
- ): 'top' | 'edge' | 'wall' | null {
254
+ ): 'top' | 'edge' | 'wall' | 'heel' | null {
252
255
  const materialIndex = getIntersectionMaterialIndex(getEventObject(event), event.faceIndex)
253
256
  if (materialIndex === 3) return 'top'
257
+ if (materialIndex === ROOF_HEEL_MATERIAL_INDEX) return 'heel'
254
258
  if (materialIndex === 0) return 'edge'
255
259
  if (materialIndex === 1 || materialIndex === 2) return 'wall'
256
260
 
@@ -451,14 +455,6 @@ function resolveSelectModeNodeTarget(event: NodeEvent, drill = false): AnyNode {
451
455
  return event.node
452
456
  }
453
457
 
454
- function previewMeshMaterial(mesh: Mesh, material: Material | Material[]): PaintPreviewCleanup {
455
- const previousMaterial = mesh.material
456
- mesh.material = material
457
- return () => {
458
- mesh.material = previousMaterial
459
- }
460
- }
461
-
462
458
  function previewCursor(cursor: string): PaintPreviewCleanup {
463
459
  const previousCursor = document.body.style.cursor
464
460
  document.body.style.cursor = cursor
@@ -467,33 +463,6 @@ function previewCursor(cursor: string): PaintPreviewCleanup {
467
463
  }
468
464
  }
469
465
 
470
- function applyRoofPaintPreview(
471
- node: RoofNode,
472
- role: 'top' | 'edge' | 'wall',
473
- material: ActivePaintMaterial,
474
- ): PaintPreviewCleanup | null {
475
- const root = getRegisteredNodeObject(node.id)
476
- const mesh = root?.getObjectByName('merged-roof') as Mesh | undefined
477
- if (!mesh) return null
478
-
479
- const previewNode = {
480
- ...node,
481
- ...buildRoofSurfaceMaterialPatch(node, role, material.material, material.materialPreset),
482
- }
483
- const previewMaterial = getRoofMaterialArray(
484
- previewNode,
485
- useViewer.getState().shading,
486
- useViewer.getState().textures,
487
- useViewer.getState().colorPreset,
488
- useViewer.getState().sceneTheme,
489
- null,
490
- useScene.getState().materials,
491
- )
492
- if (!previewMaterial) return null
493
-
494
- return previewMeshMaterial(mesh, previewMaterial)
495
- }
496
-
497
466
  // Chimney + dormer paint dispatch lives on their NodeDefinition's
498
467
  // `capabilities.paint` (see packages/nodes/src/{chimney,dormer}/
499
468
  // paint.ts). The generic registry-driven arm in this file consults
@@ -518,16 +487,14 @@ const HIGHLIGHT_PROFILES = {
518
487
  delete: {
519
488
  color: new Color('#dc2626'),
520
489
  blend: 0.76,
521
- emissiveBlend: 0.92,
522
- emissiveIntensity: 0.46,
490
+ emissive: 0.92,
523
491
  },
524
492
  selection: {
525
493
  // Keep the real material/texture readable: no albedo tint, just a gentle
526
494
  // indigo emissive glow so it reads as selected.
527
495
  color: new Color('#818cf8'),
528
496
  blend: 0,
529
- emissiveBlend: 0.4,
530
- emissiveIntensity: 0.12,
497
+ emissive: 0.12,
531
498
  },
532
499
  } as const
533
500
 
@@ -582,15 +549,7 @@ function createHighlightedMaterial(material: Material, kind: HighlightKind): Mat
582
549
  highlightedMaterial.color = highlightedMaterial.color.clone().lerp(profile.color, profile.blend)
583
550
  }
584
551
 
585
- if (highlightedMaterial.emissive instanceof Color) {
586
- highlightedMaterial.emissive = highlightedMaterial.emissive
587
- .clone()
588
- .lerp(profile.color, profile.emissiveBlend)
589
- highlightedMaterial.emissiveIntensity = Math.max(
590
- highlightedMaterial.emissiveIntensity ?? 0,
591
- profile.emissiveIntensity,
592
- )
593
- }
552
+ tintEmissive(highlightedMaterial, profile.color, profile.emissive)
594
553
 
595
554
  if (typeof highlightedMaterial.opacity === 'number' && highlightedMaterial.opacity < 1) {
596
555
  highlightedMaterial.transparent = true
@@ -598,7 +557,7 @@ function createHighlightedMaterial(material: Material, kind: HighlightKind): Mat
598
557
  }
599
558
 
600
559
  highlightedMaterial.needsUpdate = true
601
- return highlightedMaterial
560
+ return markHighlight(highlightedMaterial)
602
561
  }
603
562
 
604
563
  function createHighlightedMaterials(
@@ -892,9 +851,7 @@ export const SelectionManager = () => {
892
851
 
893
852
  // Registry-driven paint dispatch — kinds that declare
894
853
  // `capabilities.paint` route hover / click / preview through
895
- // their definition. Wall, chimney, and dormer use this; legacy
896
- // roof / stair / single-surface arms below stay until they
897
- // migrate too.
854
+ // their definition (wall, chimney, dormer, roof, …).
898
855
  const paintCap = nodeRegistry.get(node.type)?.capabilities?.paint
899
856
  if (paintCap) {
900
857
  const materialIndex = getIntersectionMaterialIndex(getEventObject(event), event.faceIndex)
@@ -1063,51 +1020,7 @@ export const SelectionManager = () => {
1063
1020
  }
1064
1021
  }
1065
1022
 
1066
- // A segment paints through its definition's `capabilities.paint`; the merged shell's hit is
1067
- // the roof's, and fans to it.
1068
- if (node.type === 'roof') {
1069
- const role = resolveRoofMaterialTarget(event as RoofEvent)
1070
- if (isPaintPicking()) return pickInteraction(node, role, event, null)
1071
- const compatible = role !== null && paintEnabled
1072
- return {
1073
- key: `roof:${node.id}:${role ?? 'unsupported'}:${eraser ? 'erase' : 'paint'}`,
1074
- hoveredId: node.id as AnyNodeId,
1075
- hoverMode: compatible ? (eraser ? 'erase-ready' : 'paint-ready') : 'paint-disabled',
1076
- // Roof isn't on the slot model (role-specific fields, custom commit),
1077
- // so it offers only the single surface — but still labels it.
1078
- paintHover:
1079
- compatible && role
1080
- ? {
1081
- scopes: ['single'],
1082
- slotLabel: slotDisplayLabel(node, role),
1083
- nodeNoun: 'roof',
1084
- }
1085
- : null,
1086
- apply:
1087
- compatible && role
1088
- ? () => {
1089
- useScene
1090
- .getState()
1091
- .updateNode(
1092
- node.id as AnyNodeId,
1093
- buildRoofSurfaceMaterialPatch(
1094
- node,
1095
- role,
1096
- paintSpec.material,
1097
- paintSpec.materialPreset,
1098
- ),
1099
- )
1100
- }
1101
- : null,
1102
- preview:
1103
- compatible && role
1104
- ? () => applyRoofPaintPreview(node, role, paintSpec)
1105
- : () => previewCursor('not-allowed'),
1106
- }
1107
- }
1108
-
1109
- // Only `roof` reaches a legacy paint arm (above) — every
1110
- // other paintable kind declares `capabilities.paint` and returns from the
1023
+ // Every paintable kind declares `capabilities.paint` and returns from the
1111
1024
  // registry-driven dispatch at the top of this function.
1112
1025
 
1113
1026
  const disabledNodeTypes = ['zone']
@@ -0,0 +1,74 @@
1
+ 'use client'
2
+
3
+ import { type SeparatorNode, sceneRegistry } from '@pascal-app/core'
4
+ import { type ThreeEvent, useThree } from '@react-three/fiber'
5
+ import { useState } from 'react'
6
+ import { OrthographicCamera, Plane, Vector2, Vector3 } from 'three'
7
+ import { runSeparatorEndDrag } from '../../lib/separator-edit'
8
+ import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
9
+ import { HandleArrow } from './handles/handle-arrow'
10
+
11
+ /**
12
+ * An invisible wall's 3D end grip, the twin of its end handle on the plan: a
13
+ * cube at the end, on the floor at `height`. Dragging it moves the end across
14
+ * that floor (`runSeparatorEndDrag`), the invisible walls joined there
15
+ * following. Rendered in the level frame.
16
+ */
17
+ export function SeparatorEndGrip({
18
+ separator,
19
+ endpoint,
20
+ height,
21
+ levelId,
22
+ }: {
23
+ separator: SeparatorNode
24
+ endpoint: 'start' | 'end'
25
+ height: number
26
+ levelId: string
27
+ }) {
28
+ const [hover, setHover] = useState(false)
29
+ const { camera, raycaster, gl } = useThree()
30
+ const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
31
+ const [x, z] = separator[endpoint]
32
+ const onPointerDown = (event: ThreeEvent<PointerEvent>) => {
33
+ if (event.button !== 0) return
34
+ event.stopPropagation()
35
+ suppressBoxSelectForPointer(event)
36
+ const level = sceneRegistry.nodes.get(levelId)
37
+ if (!level) return
38
+ level.updateWorldMatrix(true, false)
39
+ const toLocal = level.matrixWorld.clone().invert()
40
+ const floor = new Plane().setFromNormalAndCoplanarPoint(
41
+ new Vector3(0, 1, 0).transformDirection(level.matrixWorld),
42
+ new Vector3(x, height, z).applyMatrix4(level.matrixWorld),
43
+ )
44
+ runSeparatorEndDrag({
45
+ separatorId: separator.id,
46
+ endpoint,
47
+ levelId,
48
+ toPlan: (clientX, clientY) => {
49
+ const rect = gl.domElement.getBoundingClientRect()
50
+ raycaster.setFromCamera(
51
+ new Vector2(
52
+ ((clientX - rect.left) / rect.width) * 2 - 1,
53
+ -((clientY - rect.top) / rect.height) * 2 + 1,
54
+ ),
55
+ camera,
56
+ )
57
+ const hit = raycaster.ray.intersectPlane(floor, new Vector3())
58
+ if (!hit) return null
59
+ const local = hit.applyMatrix4(toLocal)
60
+ return [local.x, local.z]
61
+ },
62
+ })
63
+ }
64
+ return (
65
+ <HandleArrow
66
+ cursor="grab"
67
+ hover={hover}
68
+ onHoverChange={setHover}
69
+ onPointerDown={onPointerDown}
70
+ placement={{ position: [x, height, z], baseScale: zoom }}
71
+ shape="tracker"
72
+ />
73
+ )
74
+ }
@@ -3,7 +3,6 @@
3
3
  import type { SiteNode } from '@pascal-app/core'
4
4
  import { sceneRegistry, useLiveNodeOverrides, useScene } from '@pascal-app/core'
5
5
  import { getSceneTheme, useViewer } from '@pascal-app/viewer'
6
- import { Html } from '@react-three/drei'
7
6
  import { createPortal, useFrame, useThree } from '@react-three/fiber'
8
7
  import { useCallback, useMemo, useRef, useState } from 'react'
9
8
  import { type Camera, type Object3D, Vector3 } from 'three'
@@ -11,6 +10,7 @@ import { groundHeightAt } from '../../lib/ground-surface'
11
10
  import { formatLinearMeasurement } from '../../lib/measurements'
12
11
  import { SITE_BOUNDARY_DRAG_LABEL } from '../../lib/site-boundary'
13
12
  import { useActiveHandleDrag } from '../../store/use-interaction-scope'
13
+ import { Html } from './localized-html'
14
14
 
15
15
  type ViewportSize = {
16
16
  width: number
@@ -205,31 +205,38 @@ describe('a sheet capture session', () => {
205
205
  const viewer = useViewer.getState()
206
206
  viewer.setWallMode('down')
207
207
  viewer.setLevelMode('exploded')
208
- viewer.setShading('solid')
208
+ viewer.setRenderQuality('low')
209
209
  viewer.setTextures(false)
210
210
  viewer.setColorPreset('white')
211
+ viewer.setTimeOfDay('night')
212
+ viewer.setWeather('overcast')
211
213
  const view = () => {
212
- const { wallMode, levelMode, shading, textures, colorPreset } = useViewer.getState()
213
- return { wallMode, levelMode, shading, textures, colorPreset }
214
+ const { wallMode, levelMode, renderQuality, textures, colorPreset, timeOfDay, weather } =
215
+ useViewer.getState()
216
+ return { wallMode, levelMode, renderQuality, textures, colorPreset, timeOfDay, weather }
214
217
  }
215
- // the shading stays the user's: the capture brings its own AO
218
+ // the quality stays the user's: the capture brings its own AO
216
219
 
217
220
  const restore = presentFinished()
218
221
  expect(view()).toEqual({
219
222
  wallMode: 'up',
220
223
  levelMode: 'stacked',
221
- shading: 'solid',
224
+ renderQuality: 'low',
222
225
  textures: true,
223
226
  colorPreset: 'clay',
227
+ timeOfDay: 'midday',
228
+ weather: 'fair',
224
229
  })
225
230
 
226
231
  restore()
227
232
  expect(view()).toEqual({
228
233
  wallMode: 'down',
229
234
  levelMode: 'exploded',
230
- shading: 'solid',
235
+ renderQuality: 'low',
231
236
  textures: false,
232
237
  colorPreset: 'white',
238
+ timeOfDay: 'night',
239
+ weather: 'overcast',
233
240
  })
234
241
  })
235
242
 
@@ -239,6 +246,8 @@ describe('a sheet capture session', () => {
239
246
  viewer.setLevelMode('stacked')
240
247
  viewer.setTextures(true)
241
248
  viewer.setColorPreset('clay')
249
+ viewer.setTimeOfDay('midday')
250
+ viewer.setWeather('fair')
242
251
  let writes = 0
243
252
  const unsubscribe = useViewer.subscribe(() => {
244
253
  writes++
@@ -16,6 +16,7 @@ import {
16
16
  GRID_LAYER,
17
17
  heroCameraPose,
18
18
  holdLiveFrame,
19
+ itemLightsForCapture,
19
20
  pendingSceneBuildCount,
20
21
  presentLevelsForCapture,
21
22
  refreshIsolation,
@@ -437,13 +438,15 @@ const HOLD_MAX_MS = 300_000
437
438
  * What the sheets' pictures are drawn in, whatever the user's view: full-
438
439
  * height walls, the storeys stacked at their true heights, the materials'
439
440
  * own textures and colours (a white or blueprint palette is a view, not the
440
- * house). The shading stays the user's: the capture brings its own AO.
441
+ * house), lit by a fair midday. The shading stays the user's: the capture brings its own AO.
441
442
  */
442
443
  const FINISHED_PRESENTATION = {
443
444
  wallMode: 'up',
444
445
  levelMode: 'stacked',
445
446
  textures: true,
446
447
  colorPreset: 'clay',
448
+ timeOfDay: 'midday',
449
+ weather: 'fair',
447
450
  } as const
448
451
 
449
452
  /** The viewer switched to FINISHED_PRESENTATION; the returned function puts the user's view back. */
@@ -470,6 +473,25 @@ export function presentFinished(): () => void {
470
473
  viewer.setColorPreset(FINISHED_PRESENTATION.colorPreset)
471
474
  restores.push(() => useViewer.getState().setColorPreset(colorPreset))
472
475
  }
476
+ if (
477
+ viewer.timeOfDay !== FINISHED_PRESENTATION.timeOfDay ||
478
+ viewer.timeOfDayMinutes !== null ||
479
+ viewer.skyLive
480
+ ) {
481
+ const { timeOfDay, timeOfDayMinutes, skyLive } = viewer
482
+ viewer.setTimeOfDay(FINISHED_PRESENTATION.timeOfDay)
483
+ restores.push(() => {
484
+ const next = useViewer.getState()
485
+ next.setTimeOfDay(timeOfDay)
486
+ if (timeOfDayMinutes !== null) next.setTimeOfDayMinutes(timeOfDayMinutes)
487
+ if (skyLive) next.setSkyLive(true)
488
+ })
489
+ }
490
+ if (viewer.weather !== FINISHED_PRESENTATION.weather) {
491
+ const weather = viewer.weather
492
+ viewer.setWeather(FINISHED_PRESENTATION.weather)
493
+ restores.push(() => useViewer.getState().setWeather(weather))
494
+ }
473
495
  return () => {
474
496
  for (const restore of restores.reverse()) restore()
475
497
  }
@@ -995,6 +1017,9 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
995
1017
  } else if (snapLevels || pose || cameraPose) {
996
1018
  exteriorGroundForCapture(restore, Boolean(cameraPose || pose), thumbnailCamera)
997
1019
  }
1020
+ // Last, with the camera where the picture is taken from: the item lights that
1021
+ // matter to it, at full strength.
1022
+ restore(itemLightsForCapture(scene, thumbnailCamera))
998
1023
  }
999
1024
  // The scene pass is a FRAME-updated node: it renders once per node
1000
1025
  // frame, and only the animation loop advances that frame. With the
@@ -19,6 +19,8 @@ import { resolveGenericFloorplanGridEventPoint } from '../../lib/floorplan-grid-
19
19
  import { snapRegisteredDraftPoint } from '../../lib/registered-draft-snap'
20
20
  import { resolveSlabPlanPointSnap } from '../../lib/slab-plan-snap'
21
21
  import { commitWallPolygonDraft, wallPolygonDraftWalls } from '../../lib/wall-polygon-draft'
22
+ import { wallDraftIsRun } from '../../lib/wall-pull'
23
+ import { isWallRunFree, snapWallRunDraftPoint } from '../../lib/wall-run-snap'
22
24
  import useAlignmentGuides from '../../store/use-alignment-guides'
23
25
  import useEditor, { isAngleSnapActive, isMagneticSnapActive } from '../../store/use-editor'
24
26
  import usePlacementPreview from '../../store/use-placement-preview'
@@ -52,6 +54,8 @@ type UseFloorplanBackgroundPlacementArgs = {
52
54
  wall: WallNode
53
55
  } | null
54
56
  floorplanOpeningLocalY: number
57
+ /** An open wall run's start and the direction of its last wall (null before one). */
58
+ getWallRunContext: () => { runStart: WallPlanPoint | null; reference: WallPlanPoint | null }
55
59
  getSnappedFloorplanPoint: (point: WallPlanPoint) => WallPlanPoint
56
60
  handleCeilingItemPlacementClick: (
57
61
  planPoint: WallPlanPoint,
@@ -72,12 +76,9 @@ type UseFloorplanBackgroundPlacementArgs = {
72
76
  isZoneBuildActive: boolean
73
77
  levelId: string | null
74
78
  registryToolOwnsSnapping: boolean
75
- roofDraftStart: WallPlanPoint | null
76
79
  setCursorPoint: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
77
80
  setFenceDraftEnd: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
78
81
  setFenceDraftStart: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
79
- setRoofDraftEnd: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
80
- setRoofDraftStart: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
81
82
  snapWallDraftPoint: (args: {
82
83
  point: WallPlanPoint
83
84
  walls: WallNode[]
@@ -86,6 +87,7 @@ type UseFloorplanBackgroundPlacementArgs = {
86
87
  bypassSnap?: boolean
87
88
  step?: number
88
89
  gridSnap?: (point: WallPlanPoint) => WallPlanPoint
90
+ captureWallBodies?: boolean
89
91
  }) => WallPlanPoint
90
92
  snapPolygonDraftPoint: (args: {
91
93
  point: WallPlanPoint
@@ -95,10 +97,9 @@ type UseFloorplanBackgroundPlacementArgs = {
95
97
  toPoint2D: (point: WallPlanPoint) => { x: number; y: number }
96
98
  walls: WallNode[]
97
99
  /**
98
- * Snap a building-local plan point to the world XZ grid at `step`.
99
- * Injected so the hook doesn't have to know the building's rotation
100
- * or position — used by wall / fence branches that snap at variable
101
- * step.
100
+ * Snap a building-local plan point onto the building's grid lattice at
101
+ * `step` (the lattice the plan draws, which turns with the building).
102
+ * Injected — used by wall / fence branches that snap at variable step.
102
103
  */
103
104
  worldGridSnap: (point: WallPlanPoint, step: number) => WallPlanPoint
104
105
  }
@@ -115,6 +116,7 @@ export function useFloorplanBackgroundPlacement({
115
116
  findClosestWallPoint,
116
117
  floorplanOpeningLocalY,
117
118
  getSnappedFloorplanPoint,
119
+ getWallRunContext,
118
120
  handleCeilingItemPlacementClick,
119
121
  handleCeilingPlacementPoint,
120
122
  handleSlabPlacementPoint,
@@ -131,12 +133,9 @@ export function useFloorplanBackgroundPlacement({
131
133
  isZoneBuildActive,
132
134
  levelId,
133
135
  registryToolOwnsSnapping,
134
- roofDraftStart,
135
136
  setCursorPoint,
136
137
  setFenceDraftEnd,
137
138
  setFenceDraftStart,
138
- setRoofDraftEnd,
139
- setRoofDraftStart,
140
139
  snapWallDraftPoint,
141
140
  snapPolygonDraftPoint,
142
141
  toPoint2D,
@@ -219,24 +218,14 @@ export function useFloorplanBackgroundPlacement({
219
218
  if (isRoofBuildActive) {
220
219
  // Footprint placement (polygon context: grid / lines / off, no angle),
221
220
  // mode-driven to match the chip. Alt is force/free at commit time;
222
- // alignment display/pull follows the active magnetic mode.
221
+ // alignment display/pull follows the active magnetic mode. The roof
222
+ // tool snaps the corner it commits and writes the draft rectangle from
223
+ // it, so the plan shows exactly the roof the click makes.
223
224
  const snappedPoint = alignFloorplanDraftPoint(getSnappedFloorplanPoint(planPoint), {
224
225
  applySnap: isMagneticSnapActive(),
225
226
  })
226
227
  emitFloorplanGridEvent('click', snappedPoint, event)
227
228
  setCursorPoint(snappedPoint)
228
-
229
- if (roofFootprintSource !== 'draw') {
230
- clearRoofPlacementDraft()
231
- return true
232
- }
233
-
234
- if (roofDraftStart) {
235
- clearRoofPlacementDraft()
236
- } else {
237
- setRoofDraftStart(snappedPoint)
238
- setRoofDraftEnd(snappedPoint)
239
- }
240
229
  return true
241
230
  }
242
231
 
@@ -254,8 +243,8 @@ export function useFloorplanBackgroundPlacement({
254
243
 
255
244
  if (isFenceBuildActive) {
256
245
  // Fence draft: mode-driven (matches the chip), same as the move
257
- // preview. `grid` snaps to the world XZ grid (rotation-safe via the
258
- // `gridSnap` callback), `angles` locks 15° rays from the start, `lines`
246
+ // preview. `grid` snaps to the building's grid lattice (the `gridSnap`
247
+ // callback), `angles` locks 15° rays from the start, `lines`
259
248
  // pulls onto walls / fences / alignment, `off` is free.
260
249
  const fenceStep = getSegmentGridStep()
261
250
  const fenceAngleSnap = fenceDraftStart !== null && isAngleSnapActive()
@@ -363,30 +352,48 @@ export function useFloorplanBackgroundPlacement({
363
352
  // / draftEnd state in the floor plan would never update, leaving
364
353
  // the dashed-line draft preview invisible.
365
354
  if (isWallBuildActive) {
366
- // Wall draft: mode-driven (matches the chip + the move-preview branch).
367
- // `grid` snaps to the world XZ grid (rotation-safe via `gridSnap`),
368
- // `angles` locks 15° rays from the start, `lines` pulls the endpoint
369
- // onto existing wall corners / edges + alignment, `off` is free.
370
- const wallStep = getSegmentGridStep()
371
- const wallAngleSnap = draftStart !== null && isAngleSnapActive()
372
- const wallSnapped = snapWallDraftPoint({
373
- point: planPoint,
374
- walls: [...walls, ...wallPolygonDraftWalls()],
375
- start: draftStart ?? undefined,
376
- angleSnap: wallAngleSnap,
377
- gridSnap: (p) => worldGridSnap(p, wallStep),
378
- })
379
- const wallGridBase = worldGridSnap(planPoint, wallStep)
380
- const wallLocked = wallSnapped[0] !== wallGridBase[0] || wallSnapped[1] !== wallGridBase[1]
381
- let snappedPoint = wallSnapped
382
- if (wallLocked) {
383
- useAlignmentGuides.getState().clear()
355
+ // Wall draft, matching the move-preview branch: mode-driven (matches
356
+ // the chip). `grid` snaps to the building's grid lattice (`gridSnap`),
357
+ // `angles` locks a Walls segment to 15° rays from its start, `lines`
358
+ // pulls the point onto existing wall corners / edges + alignment, `off`
359
+ // is free, and in every mode but `lines` an end aimed into a wall's body
360
+ // joins that wall. A run's segment (a Polygon side, a pulled run) lands
361
+ // where the move showed it (`snapWallRunDraftPoint`).
362
+ let snappedPoint: WallPlanPoint
363
+ if (draftStart && wallDraftIsRun()) {
364
+ const run = getWallRunContext()
365
+ snappedPoint = snapWallRunDraftPoint({
366
+ point: planPoint,
367
+ start: draftStart,
368
+ runStart: run.runStart ?? draftStart,
369
+ reference: run.reference,
370
+ walls: [...walls, ...wallPolygonDraftWalls()],
371
+ free: isWallRunFree(event),
372
+ }).point
384
373
  } else {
385
- // Alignment lines are shown in every mode; the pull applies only when
386
- // magnetic ('lines') and the segment isn't angle-locked.
387
- snappedPoint = alignFloorplanDraftPoint(wallSnapped, {
388
- applySnap: isMagneticSnapActive() && !wallAngleSnap,
374
+ const wallStep = getSegmentGridStep()
375
+ const wallAngleSnap = draftStart !== null && isAngleSnapActive()
376
+ const wallSnapped = snapWallDraftPoint({
377
+ point: planPoint,
378
+ walls: [...walls, ...wallPolygonDraftWalls()],
379
+ start: draftStart ?? undefined,
380
+ angleSnap: wallAngleSnap,
381
+ gridSnap: (p) => worldGridSnap(p, wallStep),
382
+ captureWallBodies: true,
389
383
  })
384
+ const wallGridBase = worldGridSnap(planPoint, wallStep)
385
+ const wallLocked =
386
+ wallSnapped[0] !== wallGridBase[0] || wallSnapped[1] !== wallGridBase[1]
387
+ snappedPoint = wallSnapped
388
+ if (wallLocked) {
389
+ useAlignmentGuides.getState().clear()
390
+ } else {
391
+ // Alignment lines are shown in every mode; the pull applies only when
392
+ // magnetic ('lines') and the segment isn't angle-locked.
393
+ snappedPoint = alignFloorplanDraftPoint(wallSnapped, {
394
+ applySnap: isMagneticSnapActive() && !wallAngleSnap,
395
+ })
396
+ }
390
397
  }
391
398
 
392
399
  emitFloorplanGridEvent('click', snappedPoint, event)
@@ -437,12 +444,12 @@ export function useFloorplanBackgroundPlacement({
437
444
  activePolygonDraftPoints,
438
445
  ceilingDraftPoints,
439
446
  clearFencePlacementDraft,
440
- clearRoofPlacementDraft,
441
447
  clearWallPlacementDraft,
442
448
  emitFloorplanGridEvent,
443
449
  fenceDraftStart,
444
450
  fences,
445
451
  findClosestWallPoint,
452
+ getWallRunContext,
446
453
  floorplanOpeningLocalY,
447
454
  getSnappedFloorplanPoint,
448
455
  handleCeilingItemPlacementClick,
@@ -459,14 +466,10 @@ export function useFloorplanBackgroundPlacement({
459
466
  isWallBuildActive,
460
467
  isZoneBuildActive,
461
468
  levelId,
462
- roofDraftStart,
463
469
  registryToolOwnsSnapping,
464
- roofFootprintSource,
465
470
  setCursorPoint,
466
471
  setFenceDraftEnd,
467
472
  setFenceDraftStart,
468
- setRoofDraftEnd,
469
- setRoofDraftStart,
470
473
  snapWallDraftPoint,
471
474
  snapPolygonDraftPoint,
472
475
  toPoint2D,