@pascal-app/editor 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,509 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ getWallBaseElevationForNodes,
7
+ getWallEffectiveHeightForNodes,
8
+ sceneRegistry,
9
+ useLiveNodeOverrides,
10
+ useScene,
11
+ type WallFace,
12
+ type WallFaceRegion,
13
+ type WallNode,
14
+ } from '@pascal-app/core'
15
+ import { getWallFaceBaseAt, getWallFinishData, useViewer } from '@pascal-app/viewer'
16
+ import { createPortal, useFrame, useThree } from '@react-three/fiber'
17
+ import { useEffect, useMemo, useRef, useState } from 'react'
18
+ import {
19
+ BufferAttribute,
20
+ BufferGeometry,
21
+ Color,
22
+ DoubleSide,
23
+ type Group,
24
+ type Mesh,
25
+ type Object3D,
26
+ OrthographicCamera,
27
+ Ray,
28
+ Vector3,
29
+ } from 'three'
30
+ import { MeshBasicNodeMaterial } from 'three/webgpu'
31
+ import type { WallRegionBounds } from '../../lib/paint-regions'
32
+ import { updateWallRegion } from '../../lib/paint-regions'
33
+ import { sfxEmitter } from '../../lib/sfx-bus'
34
+ import { resolveWallFaceBase } from '../../lib/wall-region-face'
35
+ import {
36
+ buildOutlineRibbon,
37
+ faceSurfacePoint,
38
+ getWallFaceSurface,
39
+ intersectFaceSurface,
40
+ type RegionEdge,
41
+ type RegionHandlePlacement,
42
+ regionHandlePlacements,
43
+ regionOutline,
44
+ resolveRegionBoundDrag,
45
+ resolveRegionBounds,
46
+ type WallFaceSurface,
47
+ wallLocalMatrix,
48
+ withRegionBounds,
49
+ } from '../../lib/wall-region-handles'
50
+ import { wallRegionSnapTargets } from '../../lib/wall-region-snap'
51
+ import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../store/use-editor'
52
+ import useInteractionScope from '../../store/use-interaction-scope'
53
+ import {
54
+ handleWallRegionDeleteKey,
55
+ installWallRegionSceneSync,
56
+ syncWallRegionSelection,
57
+ useWallRegionSelection,
58
+ } from '../../store/use-wall-region-handles-selection'
59
+ import {
60
+ ARROW_COLOR,
61
+ ARROW_HOVER_COLOR,
62
+ ARROW_SCALE,
63
+ HandleArrow,
64
+ InvisibleHandleHitArea,
65
+ NO_RAYCAST,
66
+ useInvisibleHitAreaMaterial,
67
+ } from './handles/handle-arrow'
68
+ import { type HandleDragControls, useHandleDrag } from './handles/use-handle-drag'
69
+
70
+ // Resize handles for the paint regions of the one selected wall: a thin
71
+ // outline per region and an arrow on each bounded edge, shown only while the
72
+ // region is active, hovered or being resized so they do not crowd the wall's
73
+ // own arrows. Dragging an edge previews through a live override and commits
74
+ // one `updateWallRegion`.
75
+ // Clicking an outline or handle makes that region active, and Delete then
76
+ // removes the region instead of the wall.
77
+
78
+ export const PAINT_REGION_HANDLE = 'paint-region-bound'
79
+
80
+ const OUTLINE_HALF_WIDTH = 0.006
81
+ const OUTLINE_ACTIVE_HALF_WIDTH = 0.011
82
+ const OUTLINE_HIT_HALF_WIDTH = 0.04
83
+ const NOOP_DRAG_CONTROLS: HandleDragControls = { onStart: () => {}, onEnd: () => {} }
84
+ // The arrows sit on the outline: the delay lets the pointer cross from one to the other.
85
+ const ARROW_HIDE_DELAY_MS = 200
86
+
87
+ const outlineMaterials = new Map<boolean, MeshBasicNodeMaterial>()
88
+ function outlineMaterial(active: boolean) {
89
+ let material = outlineMaterials.get(active)
90
+ if (!material) {
91
+ material = new MeshBasicNodeMaterial({
92
+ color: new Color(active ? ARROW_HOVER_COLOR : ARROW_COLOR),
93
+ side: DoubleSide,
94
+ transparent: true,
95
+ opacity: active ? 1 : 0.8,
96
+ depthTest: false,
97
+ depthWrite: false,
98
+ })
99
+ outlineMaterials.set(active, material)
100
+ }
101
+ return material
102
+ }
103
+
104
+ function isOwnScope(
105
+ scope: ReturnType<typeof useInteractionScope.getState>['scope'],
106
+ wallId: string | undefined,
107
+ ) {
108
+ return (
109
+ scope.kind === 'handle-drag' && scope.handle === PAINT_REGION_HANDLE && scope.nodeId === wallId
110
+ )
111
+ }
112
+
113
+ export function WallRegionHandles() {
114
+ const selectedIds = useViewer((state) => state.selection.selectedIds)
115
+ const mode = useEditor((state) => state.mode)
116
+ const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered)
117
+ const scope = useInteractionScope((state) => state.scope)
118
+ const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
119
+ const wall = useScene((state) => {
120
+ const node = selectedId ? state.nodes[selectedId as AnyNodeId] : null
121
+ return node?.type === 'wall' && node.faceRegions?.length ? node : null
122
+ })
123
+
124
+ useEffect(() => syncWallRegionSelection(selectedIds), [selectedIds])
125
+
126
+ // Capture phase, so an active region takes Delete before the global
127
+ // use-keyboard arm deletes the wall.
128
+ useEffect(() => {
129
+ const onKeyDown = (event: KeyboardEvent) => {
130
+ handleWallRegionDeleteKey(event)
131
+ }
132
+ window.addEventListener('keydown', onKeyDown, true)
133
+ const uninstallSceneSync = installWallRegionSceneSync()
134
+ return () => {
135
+ window.removeEventListener('keydown', onKeyDown, true)
136
+ uninstallSceneSync()
137
+ }
138
+ }, [])
139
+
140
+ const ownScope = isOwnScope(scope, wall?.id)
141
+ const shouldRender =
142
+ Boolean(wall) &&
143
+ mode === 'select' &&
144
+ (ownScope || (scope.kind === 'idle' && !isFloorplanHovered))
145
+
146
+ if (!(shouldRender && wall)) return null
147
+ return <WallRegionHandlesForWall wall={wall} />
148
+ }
149
+
150
+ function useLevelObject(parentId: string | null | undefined) {
151
+ const [levelObject, setLevelObject] = useState<Object3D | null>(() =>
152
+ parentId ? (sceneRegistry.nodes.get(parentId) ?? null) : null,
153
+ )
154
+ useEffect(() => {
155
+ let frameId = 0
156
+ const resolve = () => {
157
+ const next = parentId ? (sceneRegistry.nodes.get(parentId) ?? null) : null
158
+ setLevelObject(next)
159
+ if (!next) frameId = window.requestAnimationFrame(resolve)
160
+ }
161
+ resolve()
162
+ return () => {
163
+ if (frameId) window.cancelAnimationFrame(frameId)
164
+ }
165
+ }, [parentId])
166
+ return levelObject
167
+ }
168
+
169
+ type WallFrame = {
170
+ wall: WallNode
171
+ baseElevation: number
172
+ wallHeight: number
173
+ angle: number
174
+ levelObject: Object3D
175
+ }
176
+
177
+ function faceBaseSampler(wallId: string, face: WallFace) {
178
+ const mesh = sceneRegistry.nodes.get(wallId as AnyNodeId) as Mesh | undefined
179
+ const data = getWallFinishData(mesh?.geometry)
180
+ return (u: number) => getWallFaceBaseAt(data, face, u)
181
+ }
182
+
183
+ function regionFaceHeight(
184
+ region: WallFaceRegion,
185
+ length: number,
186
+ wallHeight: number,
187
+ baseAt: (u: number) => number,
188
+ ) {
189
+ const midU = ((region.u0 ?? 0) + (region.u1 ?? length)) / 2
190
+ return Math.max(0, wallHeight - baseAt(midU))
191
+ }
192
+
193
+ function WallRegionHandlesForWall({ wall }: { wall: WallNode }) {
194
+ const liveOverride = useLiveNodeOverrides((state) => state.overrides.get(wall.id))
195
+ const effectiveWall = useMemo(
196
+ () => (liveOverride ? ({ ...wall, ...liveOverride } as WallNode) : wall),
197
+ [wall, liveOverride],
198
+ )
199
+ const nodes = useScene((state) => state.nodes)
200
+ const levelObject = useLevelObject(wall.parentId)
201
+ if (!levelObject) return null
202
+
203
+ const frame: WallFrame = {
204
+ wall: effectiveWall,
205
+ baseElevation: getWallBaseElevationForNodes(effectiveWall, nodes),
206
+ wallHeight: getWallEffectiveHeightForNodes(effectiveWall, nodes),
207
+ angle: Math.atan2(
208
+ effectiveWall.end[1] - effectiveWall.start[1],
209
+ effectiveWall.end[0] - effectiveWall.start[0],
210
+ ),
211
+ levelObject,
212
+ }
213
+
214
+ return createPortal(
215
+ <group
216
+ position={[effectiveWall.start[0], frame.baseElevation, effectiveWall.start[1]]}
217
+ rotation={[0, -frame.angle, 0]}
218
+ >
219
+ <FaceRegionHandles face="a" frame={frame} />
220
+ <FaceRegionHandles face="b" frame={frame} />
221
+ </group>,
222
+ levelObject,
223
+ )
224
+ }
225
+
226
+ function FaceRegionHandles({ face, frame }: { face: WallFace; frame: WallFrame }) {
227
+ const { wall } = frame
228
+ const groupRef = useRef<Group>(null)
229
+ const cameraPosition = useRef(new Vector3())
230
+ const [facing, setFacing] = useState(true)
231
+ const { camera } = useThree()
232
+ // Only the frame-defining fields: region edits must not rebuild the surface.
233
+ const { start, end, curveOffset, thickness, justification } = wall
234
+ const surface = useMemo(
235
+ () => getWallFaceSurface({ start, end, curveOffset, thickness, justification }, face),
236
+ [start, end, curveOffset, thickness, justification, face],
237
+ )
238
+ const regions = (wall.faceRegions ?? []).filter((region) => region.face === face)
239
+
240
+ // A face turned away from the camera hides its handles: they draw on top of
241
+ // everything, so they would read as belonging to the near face.
242
+ useFrame(() => {
243
+ const group = groupRef.current
244
+ if (!group) return
245
+ if (isOwnScope(useInteractionScope.getState().scope, wall.id)) return
246
+ const local = group.worldToLocal(camera.getWorldPosition(cameraPosition.current))
247
+ const point = faceSurfacePoint(surface, surface.length / 2)
248
+ const next = (local.x - point.x) * point.nx + (local.z - point.z) * point.nz > 0
249
+ if (next !== facing) setFacing(next)
250
+ })
251
+
252
+ if (regions.length === 0) return null
253
+ const baseAt = faceBaseSampler(wall.id, face)
254
+ return (
255
+ <group ref={groupRef}>
256
+ {facing &&
257
+ regions.map((region) => (
258
+ <RegionEditor
259
+ baseAt={baseAt}
260
+ frame={frame}
261
+ key={region.id}
262
+ region={region}
263
+ surface={surface}
264
+ />
265
+ ))}
266
+ </group>
267
+ )
268
+ }
269
+
270
+ function useRibbonGeometry(positions: Float32Array) {
271
+ const geometry = useMemo(() => {
272
+ const next = new BufferGeometry()
273
+ next.setAttribute('position', new BufferAttribute(positions, 3))
274
+ next.computeBoundingSphere()
275
+ return next
276
+ }, [positions])
277
+ useEffect(() => () => geometry.dispose(), [geometry])
278
+ return geometry
279
+ }
280
+
281
+ function RegionEditor({
282
+ region,
283
+ surface,
284
+ frame,
285
+ baseAt,
286
+ }: {
287
+ region: WallFaceRegion
288
+ surface: WallFaceSurface
289
+ frame: WallFrame
290
+ baseAt: (u: number) => number
291
+ }) {
292
+ const { wall } = frame
293
+ const [outlineHovered, setOutlineHovered] = useState(false)
294
+ const [hoveredArrow, setHoveredArrow] = useState<RegionEdge | null>(null)
295
+ const [draggingEdge, setDraggingEdge] = useState<RegionEdge | null>(null)
296
+ const [arrowsLinger, setArrowsLinger] = useState(false)
297
+ const isActive = useWallRegionSelection(
298
+ (state) => state.active?.wallId === wall.id && state.active.regionId === region.id,
299
+ )
300
+ const faceHeight = regionFaceHeight(region, surface.length, frame.wallHeight, baseAt)
301
+ const boundsKey = [region.u0, region.u1, region.v0, region.v1, faceHeight, surface.length].join(
302
+ ':',
303
+ )
304
+ // biome-ignore lint/correctness/useExhaustiveDependencies: boundsKey captures every input
305
+ const outline = useMemo(
306
+ () => regionOutline(surface, region, faceHeight, baseAt),
307
+ [boundsKey, surface],
308
+ )
309
+ const placements = regionHandlePlacements(surface, region, faceHeight, baseAt, draggingEdge)
310
+ const hovered =
311
+ outlineHovered ||
312
+ (hoveredArrow !== null && placements.some((placement) => placement.edge === hoveredArrow))
313
+ const showArrows = isActive || hovered || arrowsLinger || draggingEdge !== null
314
+ const halfWidth = isActive || hovered ? OUTLINE_ACTIVE_HALF_WIDTH : OUTLINE_HALF_WIDTH
315
+ const ribbon = useRibbonGeometry(
316
+ useMemo(() => buildOutlineRibbon(outline, halfWidth), [outline, halfWidth]),
317
+ )
318
+ const hitRibbon = useRibbonGeometry(
319
+ useMemo(() => buildOutlineRibbon(outline, OUTLINE_HIT_HALF_WIDTH), [outline]),
320
+ )
321
+ const hitMaterial = useInvisibleHitAreaMaterial()
322
+ const select = () =>
323
+ useWallRegionSelection.getState().setActive({ wallId: wall.id, regionId: region.id })
324
+ const activateTap = useHandleDrag({ kind: 'tap', onTap: select })
325
+ const onArrowHoverChange = (edge: RegionEdge, next: boolean) =>
326
+ setHoveredArrow((current) => (next ? edge : current === edge ? null : current))
327
+
328
+ useEffect(() => {
329
+ if (hovered) {
330
+ setArrowsLinger(true)
331
+ return
332
+ }
333
+ const timer = window.setTimeout(() => setArrowsLinger(false), ARROW_HIDE_DELAY_MS)
334
+ return () => window.clearTimeout(timer)
335
+ }, [hovered])
336
+
337
+ useEffect(
338
+ () => () => {
339
+ if (document.body.style.cursor === 'pointer') document.body.style.cursor = ''
340
+ },
341
+ [],
342
+ )
343
+
344
+ return (
345
+ <>
346
+ <mesh
347
+ frustumCulled={false}
348
+ geometry={ribbon}
349
+ material={outlineMaterial(isActive || hovered)}
350
+ raycast={NO_RAYCAST}
351
+ renderOrder={1001}
352
+ />
353
+ <InvisibleHandleHitArea
354
+ geometry={hitRibbon}
355
+ material={hitMaterial}
356
+ onPointerDown={activateTap}
357
+ onPointerEnter={(event) => {
358
+ event.stopPropagation()
359
+ setOutlineHovered(true)
360
+ document.body.style.cursor = 'pointer'
361
+ }}
362
+ onPointerLeave={(event) => {
363
+ event.stopPropagation()
364
+ setOutlineHovered(false)
365
+ if (document.body.style.cursor === 'pointer') document.body.style.cursor = ''
366
+ }}
367
+ scale={1}
368
+ />
369
+ {showArrows &&
370
+ placements.map((placement) => (
371
+ <RegionEdgeHandle
372
+ frame={frame}
373
+ key={placement.edge}
374
+ onDraggingChange={setDraggingEdge}
375
+ onHoverChange={onArrowHoverChange}
376
+ onSelect={select}
377
+ placement={placement}
378
+ regionId={region.id}
379
+ />
380
+ ))}
381
+ </>
382
+ )
383
+ }
384
+
385
+ function RegionEdgeHandle({
386
+ frame,
387
+ regionId,
388
+ placement,
389
+ onSelect,
390
+ onDraggingChange,
391
+ onHoverChange,
392
+ }: {
393
+ frame: WallFrame
394
+ regionId: string
395
+ placement: RegionHandlePlacement
396
+ onSelect: () => void
397
+ onDraggingChange: (edge: RegionEdge | null) => void
398
+ onHoverChange: (edge: RegionEdge, hovered: boolean) => void
399
+ }) {
400
+ const { wall, levelObject } = frame
401
+ const [hovered, setHovered] = useState(false)
402
+ const [dragging, setDragging] = useState(false)
403
+ const { camera } = useThree()
404
+ const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
405
+ const edge: RegionEdge = placement.edge
406
+ const cursor = edge[0] === 'u' ? 'ew-resize' : 'ns-resize'
407
+
408
+ const activate = useHandleDrag({
409
+ kind: 'drag',
410
+ cursor,
411
+ dragControls: NOOP_DRAG_CONTROLS,
412
+ handleIndex: 0,
413
+ node: wall,
414
+ rideObject: levelObject,
415
+ setIsDragging: (next) => {
416
+ setDragging(next)
417
+ onDraggingChange(next ? edge : null)
418
+ },
419
+ onStart: ({ event, getPointerRay, initialNode, nodeId, rideObject }) => {
420
+ if (initialNode.type !== 'wall') return null
421
+ const regions = initialNode.faceRegions ?? []
422
+ const region = regions.find((entry) => entry.id === regionId)
423
+ if (!region) return null
424
+ onSelect()
425
+ const nodes = useScene.getState().nodes
426
+ const face = region.face
427
+ const faceSurface = getWallFaceSurface(initialNode, face)
428
+ const baseAt = faceBaseSampler(initialNode.id, face)
429
+ const wallHeight = getWallEffectiveHeightForNodes(initialNode, nodes)
430
+ const faceHeight = regionFaceHeight(region, faceSurface.length, wallHeight, baseAt)
431
+ const bounds = resolveRegionBounds(region, faceSurface.length, faceHeight)
432
+ const midU = (bounds.u0 + bounds.u1) / 2
433
+ const mesh = sceneRegistry.nodes.get(initialNode.id) as Mesh | undefined
434
+ const runs = resolveWallFaceBase(initialNode, mesh?.geometry, nodes)?.[face] ?? null
435
+ const faceTargets = wallRegionSnapTargets(initialNode, face, nodes, runs, {
436
+ excludeRegionId: regionId,
437
+ })
438
+ const snapTargets = edge[0] === 'u' ? faceTargets.u : faceTargets.v
439
+
440
+ rideObject.updateWorldMatrix(true, false)
441
+ const toWallLocal = rideObject.matrixWorld
442
+ .clone()
443
+ .multiply(wallLocalMatrix(initialNode, getWallBaseElevationForNodes(initialNode, nodes)))
444
+ .invert()
445
+ const ray = new Ray()
446
+ const pointerValue = (worldRay: Ray) => {
447
+ ray.copy(worldRay).applyMatrix4(toWallLocal)
448
+ const hit = intersectFaceSurface(faceSurface, ray.origin, ray.direction)
449
+ if (!hit) return null
450
+ return edge[0] === 'u' ? hit.u : hit.y - baseAt(midU)
451
+ }
452
+ const initialValue = bounds[edge]
453
+ const initialPointer =
454
+ pointerValue(getPointerRay(event.nativeEvent.clientX, event.nativeEvent.clientY, ray)) ??
455
+ initialValue
456
+ let lastBounds: WallRegionBounds | null = null
457
+
458
+ return {
459
+ onBegin: () => {
460
+ useInteractionScope
461
+ .getState()
462
+ .begin({ kind: 'handle-drag', nodeId, handle: PAINT_REGION_HANDLE })
463
+ },
464
+ onEnd: () => {
465
+ useInteractionScope.getState().endIf((scope) => isOwnScope(scope, nodeId))
466
+ },
467
+ move: ({ event: moveEvent, getPointerRay: getMoveRay }) => {
468
+ const value = pointerValue(getMoveRay(moveEvent.clientX, moveEvent.clientY, new Ray()))
469
+ if (value === null) return null
470
+ const next = resolveRegionBoundDrag({
471
+ region,
472
+ edge,
473
+ raw: initialValue + (value - initialPointer),
474
+ length: faceSurface.length,
475
+ faceHeight,
476
+ gridStep: isGridSnapActive() ? useEditor.getState().gridSnapStep : null,
477
+ edgeSnap: isMagneticSnapActive(),
478
+ snapTargets,
479
+ })
480
+ if (lastBounds && lastBounds[edge] !== next[edge]) sfxEmitter.emit('sfx:resize')
481
+ lastBounds = next
482
+ return { faceRegions: withRegionBounds(regions, regionId, next) } as Partial<AnyNode>
483
+ },
484
+ commit: () => {
485
+ if (lastBounds) updateWallRegion(nodeId, regionId, lastBounds)
486
+ },
487
+ }
488
+ },
489
+ })
490
+
491
+ return (
492
+ <group rotation={[0, placement.yaw, 0]} position={placement.position}>
493
+ <HandleArrow
494
+ activeCursor={cursor}
495
+ cursor={cursor}
496
+ hover={hovered || dragging}
497
+ indicatorRotation={[Math.PI / 2, placement.tipAngle, 0]}
498
+ onHoverChange={(next) => {
499
+ setHovered(next)
500
+ onHoverChange(edge, next)
501
+ }}
502
+ onPointerDown={activate}
503
+ placement={{ position: [0, 0, 0], baseScale: zoom * ARROW_SCALE }}
504
+ shape="chevron"
505
+ thin
506
+ />
507
+ </group>
508
+ )
509
+ }
@@ -155,6 +155,9 @@ function getWallTopY(wall: WallNode, nodes: Readonly<Record<string, AnyNode>>) {
155
155
  wall.curveOffset ?? 0,
156
156
  wall.thickness,
157
157
  wall.supportSlabId,
158
+ undefined,
159
+ undefined,
160
+ wall.justification,
158
161
  )
159
162
  const planeTop = getWallPlaneTop(wall, levelId, nodes as Record<string, AnyNode>)
160
163
  return resolveWallTop(wall, planeTop, support.elevation) + WALL_TOP_HIGHLIGHT_LIFT
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { Icon } from '@iconify/react'
4
4
  import { memo, useLayoutEffect, useMemo, useRef, useState } from 'react'
5
+ import { wallCursorIcon } from '../../lib/tool-cursor-icon'
5
6
  import useEditor, { type FloorplanSelectionTool } from '../../store/use-editor'
6
7
  import { useFloorplanDraftPreview } from '../../store/use-floorplan-draft-preview'
7
8
  import { furnishTools } from '../ui/action-menu/furnish-tools'
@@ -45,8 +46,8 @@ export const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndi
45
46
  }: FloorplanCursorIndicatorOverlayProps) {
46
47
  const mode = useEditor((state) => state.mode)
47
48
  const tool = useEditor((state) => state.tool)
48
- const structureLayer = useEditor((state) => state.structureLayer)
49
49
  const catalogCategory = useEditor((state) => state.catalogCategory)
50
+ const wallMode = useEditor((state) => state.continuationByContext.wall)
50
51
  const cursorPoint = useFloorplanDraftPreview((state) => state.cursorPoint)
51
52
  const anchorRef = useRef<HTMLDivElement>(null)
52
53
  const [projectedCursorPosition, setProjectedCursorPosition] =
@@ -65,15 +66,18 @@ export const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndi
65
66
  return furnishTools.find((entry) => entry.catalogCategory === catalogCategory) ?? null
66
67
  }
67
68
 
69
+ // The wall shows its Rooms variant, like the 3D cursor bubble.
70
+ if (tool === 'wall') return wallCursorIcon(wallMode)
71
+
68
72
  return structureTools.find((entry) => entry.id === tool) ?? null
69
- }, [catalogCategory, mode, movingOpeningType, tool])
73
+ }, [catalogCategory, mode, movingOpeningType, tool, wallMode])
70
74
 
71
75
  const indicator = useMemo<FloorplanCursorIndicator | null>(() => {
72
76
  if (activeFloorplanToolConfig) {
73
77
  return { kind: 'asset', iconSrc: activeFloorplanToolConfig.iconSrc }
74
78
  }
75
79
 
76
- if (mode === 'select' && floorplanSelectionTool === 'marquee' && structureLayer !== 'zones') {
80
+ if (mode === 'select' && floorplanSelectionTool === 'marquee') {
77
81
  return { kind: 'icon', icon: 'mdi:select-drag' }
78
82
  }
79
83
 
@@ -86,7 +90,7 @@ export const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndi
86
90
  }
87
91
 
88
92
  return null
89
- }, [activeFloorplanToolConfig, floorplanSelectionTool, mode, structureLayer])
93
+ }, [activeFloorplanToolConfig, floorplanSelectionTool, mode])
90
94
 
91
95
  useLayoutEffect(() => {
92
96
  const anchor = anchorRef.current
@@ -37,8 +37,7 @@ import useInteractionScope, { useMovingNode } from '../../store/use-interaction-
37
37
  import {
38
38
  classifyParticipant,
39
39
  collectParticipants,
40
- computeGroupBox,
41
- expandToComponent,
40
+ computeGroupPlanBox,
42
41
  type GroupPlanBounds,
43
42
  groupPlanBounds,
44
43
  levelFrame,
@@ -128,9 +127,8 @@ export function startFloorplanGroupMove(
128
127
  const participantIds = selectedIds.filter(
129
128
  (id) => classifyParticipant(nodes[id as AnyNodeId], levelId, nodes) !== null,
130
129
  )
131
- // Move the full connected wall/fence component, mirroring the 3D gizmo.
132
- const fullIds = expandToComponent(participantIds, nodes, levelId)
133
- const { starts, links } = collectParticipants(fullIds, nodes, levelId)
130
+ // Only the selection moves; connected walls stretch through `links`, as in 3D.
131
+ const { starts, links } = collectParticipants(participantIds, nodes, levelId)
134
132
  if (starts.length === 0) return null
135
133
  const affectedIds: AnyNodeId[] = [...starts.map((s) => s.id), ...links.map((l) => l.id)]
136
134
 
@@ -144,10 +142,9 @@ export function startFloorplanGroupMove(
144
142
  const candidates = collectAlignmentAnchors(staticNodes, '', levelId)
145
143
 
146
144
  // The group aligns as one rigid footprint: its bbox corners + center are
147
- // the moving anchors. `computeGroupBox` is world-space (the 3D scene stays
148
- // mounted under every view mode); plan coords are level-frame, so convert.
145
+ // the moving anchors, measured in the level frame like the plan itself.
149
146
  const { inverse: frameInv } = levelFrame(levelId)
150
- const restBounds = groupPlanBounds(computeGroupBox(fullIds), starts, frameInv)
147
+ const restBounds = groupPlanBounds(starts, frameInv)
151
148
  if (!restBounds) return null
152
149
  const restAnchors = bboxCornerAnchors(
153
150
  'group-move',
@@ -441,12 +438,11 @@ export function startFloorplanGroupRotate(event: {
441
438
  (id) => classifyParticipant(nodes[id as AnyNodeId], levelId, nodes) !== null,
442
439
  )
443
440
  if (participantIds.length === 0) return false
444
- const fullIds = expandToComponent(participantIds, nodes, levelId)
445
- const { starts, links } = collectParticipants(fullIds, nodes, levelId)
441
+ const { starts, links } = collectParticipants(participantIds, nodes, levelId)
446
442
  if (starts.length === 0) return false
447
443
  const affectedIds: AnyNodeId[] = [...starts.map((s) => s.id), ...links.map((l) => l.id)]
448
444
  const { inverse: frameInv } = levelFrame(levelId)
449
- const bounds = groupPlanBounds(computeGroupBox(fullIds), starts, frameInv)
445
+ const bounds = groupPlanBounds(starts, frameInv)
450
446
  if (!bounds) return false
451
447
  // Same pivot as the dashed box the handles hang off (and as the 3D rotate
452
448
  // gizmo): its centre, not the anchor points' centre.
@@ -605,8 +601,9 @@ const GROUP_BOX_ROTATE_CURSOR_STYLE = {
605
601
 
606
602
  /**
607
603
  * Dashed bounding box around the current multi-selection's transformable
608
- * participants (expanded to the welded wall/fence component) — shows what a
609
- * group drag will carry along, and IS the group's drag handle: press anywhere
604
+ * participants — what a group drag carries (connected walls outside the
605
+ * selection stretch at their shared ends to stay joined) — and IS the group's
606
+ * drag handle: press anywhere
610
607
  * inside it to slide the group, click to pick it up. Holding a selection
611
608
  * modifier (Cmd/Ctrl/Shift) lets pointer events pass through so members under
612
609
  * the box can still be toggled in and out. Rides the live drag delta so it
@@ -653,7 +650,7 @@ export const FloorplanGroupSelectionBox = memo(function FloorplanGroupSelectionB
653
650
  }, [])
654
651
 
655
652
  // `meshEpoch` re-runs the measurement once the meshes settle after a scene
656
- // change (undo/redo included) — `computeGroupBox` reads mesh world bounds,
653
+ // change (undo/redo included) — `computeGroupPlanBox` reads mesh bounds,
657
654
  // which lag the `nodes` commit by a frame or two.
658
655
  const meshEpoch = useMeshSettleEpoch(nodes)
659
656
  const box = useMemo(() => {
@@ -663,17 +660,13 @@ export const FloorplanGroupSelectionBox = memo(function FloorplanGroupSelectionB
663
660
  (id) => classifyParticipant(nodes[id as AnyNodeId], levelId, nodes) !== null,
664
661
  )
665
662
  if (participantIds.length === 0) return null
666
- const fullIds = expandToComponent(participantIds, nodes, levelId)
667
- const world = computeGroupBox(fullIds)
668
- if (!world) return null
669
- const { inverse } = levelFrame(levelId)
670
- const min = world.min.clone().applyMatrix4(inverse)
671
- const max = world.max.clone().applyMatrix4(inverse)
663
+ const plan = computeGroupPlanBox(participantIds, levelId)
664
+ if (!plan) return null
672
665
  return {
673
- x: Math.min(min.x, max.x),
674
- z: Math.min(min.z, max.z),
675
- width: Math.abs(max.x - min.x),
676
- depth: Math.abs(max.z - min.z),
666
+ x: plan.minX,
667
+ z: plan.minZ,
668
+ width: plan.maxX - plan.minX,
669
+ depth: plan.maxZ - plan.minZ,
677
670
  }
678
671
  }, [selectedIds, levelId, nodes, meshEpoch])
679
672