@pascal-app/editor 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (440) hide show
  1. package/package.json +11 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +202 -57
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +36 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,466 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ emitter,
7
+ sceneRegistry,
8
+ useScene,
9
+ type WallFace,
10
+ type WallNode,
11
+ } from '@pascal-app/core'
12
+ import { getSceneTheme, setSurfaceRaycastLayers, useViewer } from '@pascal-app/viewer'
13
+ import { useFrame, useThree } from '@react-three/fiber'
14
+ import { useEffect, useMemo, useRef } from 'react'
15
+ import {
16
+ BufferGeometry,
17
+ type Camera,
18
+ DoubleSide,
19
+ Float32BufferAttribute,
20
+ type Group,
21
+ Matrix4,
22
+ type Mesh,
23
+ type Object3D,
24
+ Raycaster,
25
+ Vector2,
26
+ } from 'three'
27
+ import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
28
+ import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
29
+ import { EDITOR_LAYER } from '../../../lib/constants'
30
+ import { formatLinearMeasurement } from '../../../lib/measurements'
31
+ import { paintRegionTargets, usePaintRegionMode } from '../../../lib/paint-region-mode'
32
+ import { isTypingTarget } from '../../../lib/typing-target'
33
+ import {
34
+ activeWallRegionGesture,
35
+ activeWallRegionTarget,
36
+ cancelWallRegion,
37
+ dragWallRegion,
38
+ hoverWallRegion,
39
+ pressWallRegion,
40
+ releaseWallRegion,
41
+ useWallPaintRegionSession,
42
+ type WallRegionHit,
43
+ type WallRegionPreview,
44
+ } from '../../../lib/wall-paint-region-session'
45
+ import {
46
+ type FaceSpan,
47
+ facePoint,
48
+ faceSpanStrips,
49
+ faceStripOutline,
50
+ faceStripTriangles,
51
+ projectLocalRayToFace,
52
+ resolveWallFaceBase,
53
+ wallFaceExtent,
54
+ wallFaceFrame,
55
+ wallHitFaceUV,
56
+ } from '../../../lib/wall-region-face'
57
+ import {
58
+ DEFAULT_LINE_SNAP_TOLERANCE,
59
+ faceBaseAt,
60
+ resolveWallRegionSnapMode,
61
+ type WallRegionSnap,
62
+ } from '../../../lib/wall-region-snap'
63
+ import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../../store/use-editor'
64
+ import { DraftMeasurementLabel } from '../shared/draft-measurement-label'
65
+ import { usePaintTint } from './use-paint-tint'
66
+
67
+ const DRAFT_COLOR = '#818cf8'
68
+ const CORNER_HALF_SIZE = 0.04
69
+ const FILL_OFFSET = 0.004
70
+ const LINE_OFFSET = 0.006
71
+
72
+ const noRaycast = () => {}
73
+ const overlayMaterial = (opacity: number) =>
74
+ new MeshBasicNodeMaterial({
75
+ color: DRAFT_COLOR,
76
+ depthWrite: false,
77
+ opacity,
78
+ polygonOffset: true,
79
+ polygonOffsetFactor: -2,
80
+ polygonOffsetUnits: -2,
81
+ side: DoubleSide,
82
+ transparent: true,
83
+ })
84
+ const fillMaterial = overlayMaterial(0.35)
85
+ const lineMaterial = overlayMaterial(0.95)
86
+ const outlineMaterial = new LineBasicNodeMaterial({
87
+ color: DRAFT_COLOR,
88
+ depthWrite: false,
89
+ transparent: true,
90
+ })
91
+
92
+ function wallRegionActive(): boolean {
93
+ return paintRegionTargets(usePaintRegionMode.getState().mode).wall
94
+ }
95
+
96
+ function visibleSurface(object: Object3D) {
97
+ for (let node: Object3D | null = object; node; node = node.parent)
98
+ if (!node.visible || node.userData.wallHidden === true) return false
99
+ return true
100
+ }
101
+
102
+ type PointerSample = Pick<PointerEvent, 'clientX' | 'clientY' | 'target' | 'altKey'>
103
+
104
+ const sample = (event: PointerSample): PointerSample => ({
105
+ clientX: event.clientX,
106
+ clientY: event.clientY,
107
+ target: event.target,
108
+ altKey: event.altKey,
109
+ })
110
+
111
+ function pointerRaycaster(event: PointerSample, canvas: HTMLCanvasElement, camera: Camera) {
112
+ const bounds = canvas.getBoundingClientRect()
113
+ const raycaster = new Raycaster()
114
+ raycaster.setFromCamera(
115
+ new Vector2(
116
+ ((event.clientX - bounds.left) / bounds.width) * 2 - 1,
117
+ 1 - ((event.clientY - bounds.top) / bounds.height) * 2,
118
+ ),
119
+ camera,
120
+ )
121
+ setSurfaceRaycastLayers(raycaster.layers)
122
+ return raycaster
123
+ }
124
+
125
+ function regionHit(
126
+ wall: WallNode,
127
+ mesh: Mesh,
128
+ face: WallFace,
129
+ u: number,
130
+ v: number,
131
+ nodes: Readonly<Record<string, AnyNode | undefined>>,
132
+ ): WallRegionHit {
133
+ const faceBase = resolveWallFaceBase(wall, mesh.geometry, nodes)
134
+ return {
135
+ wallId: wall.id,
136
+ face,
137
+ u,
138
+ v,
139
+ length: Math.hypot(wall.end[0] - wall.start[0], wall.end[1] - wall.start[1]),
140
+ extent: wallFaceExtent(wall, face, mesh.geometry),
141
+ runs: faceBase?.[face] ?? null,
142
+ }
143
+ }
144
+
145
+ /** The nearest visible wall face of the current level under the pointer. */
146
+ function hoverHit(raycaster: Raycaster): WallRegionHit | null {
147
+ const levelId = useViewer.getState().selection.levelId
148
+ const nodes = useScene.getState().nodes
149
+ const level = levelId ? nodes[levelId as AnyNodeId] : undefined
150
+ if (level?.type !== 'level') return null
151
+ let best: {
152
+ wall: WallNode
153
+ mesh: Mesh
154
+ distance: number
155
+ hit: ReturnType<Raycaster['intersectObject']>[number]
156
+ } | null = null
157
+ for (const childId of level.children) {
158
+ const wall = nodes[childId as AnyNodeId]
159
+ if (wall?.type !== 'wall') continue
160
+ const mesh = sceneRegistry.nodes.get(wall.id) as Mesh | undefined
161
+ if (!(mesh?.isMesh && visibleSurface(mesh))) continue
162
+ const hit = raycaster.intersectObject(mesh, false)[0]
163
+ if (!hit || (best && hit.distance >= best.distance)) continue
164
+ best = { wall, mesh, distance: hit.distance, hit }
165
+ }
166
+ if (!best) return null
167
+ const local = best.mesh.worldToLocal(best.hit.point.clone())
168
+ const normal = best.hit.face?.normal
169
+ const faceBase = resolveWallFaceBase(best.wall, best.mesh.geometry, nodes)
170
+ const uv = wallHitFaceUV(
171
+ best.wall,
172
+ [local.x, local.y, local.z],
173
+ normal ? [normal.x, normal.y, normal.z] : undefined,
174
+ faceBase,
175
+ )
176
+ // Below the face base the ray has already passed through the floor, which
177
+ // the floor region gesture owns.
178
+ if (!uv || uv.v < -0.01) return null
179
+ return regionHit(best.wall, best.mesh, uv.face, uv.u, uv.v, nodes)
180
+ }
181
+
182
+ /** The pointer projected onto the pressed face, wherever it wanders. */
183
+ function dragHit(raycaster: Raycaster, target: { wallId: string; face: WallFace }) {
184
+ const nodes = useScene.getState().nodes
185
+ const wall = nodes[target.wallId as AnyNodeId]
186
+ const mesh = sceneRegistry.nodes.get(target.wallId) as Mesh | undefined
187
+ if (wall?.type !== 'wall' || !mesh) return null
188
+ mesh.updateWorldMatrix(true, false)
189
+ const ray = raycaster.ray.clone().applyMatrix4(new Matrix4().copy(mesh.matrixWorld).invert())
190
+ const point = projectLocalRayToFace(
191
+ wallFaceFrame(wall, target.face),
192
+ [ray.origin.x, ray.origin.y, ray.origin.z],
193
+ [ray.direction.x, ray.direction.y, ray.direction.z],
194
+ )
195
+ if (!point) return null
196
+ const runs = resolveWallFaceBase(wall, mesh.geometry, nodes)?.[target.face] ?? null
197
+ return regionHit(wall, mesh, target.face, point.u, point.y - faceBaseAt(runs, point.u), nodes)
198
+ }
199
+
200
+ function currentSnap(event: { altKey: boolean }): WallRegionSnap {
201
+ return {
202
+ mode: resolveWallRegionSnapMode({
203
+ grid: isGridSnapActive(),
204
+ magnetic: isMagneticSnapActive(),
205
+ alt: event.altKey,
206
+ }),
207
+ gridStep: useEditor.getState().gridSnapStep,
208
+ tolerance: DEFAULT_LINE_SNAP_TOLERANCE,
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Pointer and keyboard wiring. Hover previews on every (frame-coalesced) move;
214
+ * a left press on a wall face claims the pointer, drags on window listeners so
215
+ * the gesture follows the pointer off the wall, and commits on release. Escape
216
+ * (and ⌘Z) mid-gesture cancel it through `tool:cancel` without a write.
217
+ */
218
+ function useWallRegionPointer(canvas: HTMLCanvasElement, camera: Camera) {
219
+ useEffect(() => {
220
+ let pressed: { pointerId: number } | null = null
221
+ let frame = 0
222
+ let pending: PointerEvent | null = null
223
+ let last: PointerSample | null = null
224
+ const dropPending = () => {
225
+ if (frame) cancelAnimationFrame(frame)
226
+ frame = 0
227
+ pending = null
228
+ }
229
+ const apply = (event: PointerSample) => {
230
+ last = event
231
+ if (!wallRegionActive()) return
232
+ const raycaster = pointerRaycaster(event, canvas, camera)
233
+ if (pressed) {
234
+ const target = activeWallRegionTarget()
235
+ const hit = target ? dragHit(raycaster, target) : null
236
+ if (hit) dragWallRegion(hit, currentSnap(event))
237
+ return
238
+ }
239
+ const idle = event.target === canvas && !useViewer.getState().cameraDragging
240
+ hoverWallRegion(idle ? hoverHit(raycaster) : null, currentSnap(event))
241
+ }
242
+ const flush = () => {
243
+ frame = 0
244
+ const event = pending
245
+ pending = null
246
+ if (event) apply(sample(event))
247
+ }
248
+ const onMove = (event: PointerEvent) => {
249
+ if (pressed && event.pointerId !== pressed.pointerId) return
250
+ pending = event
251
+ if (!frame) frame = requestAnimationFrame(flush) || 0
252
+ }
253
+ // Pointer capture is the gesture owner's; it releases it however the gesture ends.
254
+ const endPress = () => {
255
+ if (!pressed) return
256
+ pressed = null
257
+ dropPending()
258
+ // The click the browser synthesizes from this release belongs to the gesture.
259
+ setTimeout(() => useViewer.getState().setInputDragging(false), 0)
260
+ }
261
+ const onDown = (event: PointerEvent) => {
262
+ if (event.button !== 0 || event.target !== canvas || pressed) return
263
+ if (useViewer.getState().cameraDragging) return
264
+ if (!wallRegionActive()) return
265
+ const hit = hoverHit(pointerRaycaster(event, canvas, camera))
266
+ if (!hit) return
267
+ event.preventDefault()
268
+ event.stopPropagation()
269
+ dropPending()
270
+ if (!pressWallRegion(hit, currentSnap(event), endPress)) return
271
+ pressed = { pointerId: event.pointerId }
272
+ activeWallRegionGesture()?.capturePointer(canvas, event.pointerId)
273
+ useViewer.getState().setInputDragging(true)
274
+ }
275
+ const onUp = (event: PointerEvent) => {
276
+ if (!pressed || event.pointerId !== pressed.pointerId) return
277
+ const final = pending
278
+ dropPending()
279
+ if (final) apply(sample(final))
280
+ event.preventDefault()
281
+ event.stopPropagation()
282
+ endPress()
283
+ releaseWallRegion()
284
+ // Show what the next press would do without waiting for a move.
285
+ if (last) apply(last)
286
+ }
287
+ const onPointerCancel = (event: PointerEvent) => {
288
+ if (!pressed || event.pointerId !== pressed.pointerId) return
289
+ cancelWallRegion()
290
+ }
291
+ const onToolCancel = () => {
292
+ if (!cancelWallRegion()) return
293
+ markToolCancelConsumed()
294
+ }
295
+ // Alt frees the snap; pressing or letting go re-evaluates where the pointer is.
296
+ const onKey = (event: KeyboardEvent) => {
297
+ if (event.key !== 'Alt' || isTypingTarget(event.target) || !last) return
298
+ apply({ ...sample(last), altKey: event.type === 'keydown' })
299
+ }
300
+ canvas.addEventListener('pointerdown', onDown, true)
301
+ window.addEventListener('pointermove', onMove, true)
302
+ window.addEventListener('pointerup', onUp, true)
303
+ window.addEventListener('pointercancel', onPointerCancel, true)
304
+ window.addEventListener('keydown', onKey, true)
305
+ window.addEventListener('keyup', onKey, true)
306
+ emitter.on('tool:cancel', onToolCancel)
307
+ return () => {
308
+ cancelWallRegion()
309
+ dropPending()
310
+ useWallPaintRegionSession.setState({ preview: null })
311
+ canvas.removeEventListener('pointerdown', onDown, true)
312
+ window.removeEventListener('pointermove', onMove, true)
313
+ window.removeEventListener('pointerup', onUp, true)
314
+ window.removeEventListener('pointercancel', onPointerCancel, true)
315
+ window.removeEventListener('keydown', onKey, true)
316
+ window.removeEventListener('keyup', onKey, true)
317
+ emitter.off('tool:cancel', onToolCancel)
318
+ }
319
+ }, [canvas, camera])
320
+ }
321
+
322
+ function positions(array: Float32Array) {
323
+ const geometry = new BufferGeometry()
324
+ geometry.setAttribute('position', new Float32BufferAttribute(array, 3))
325
+ return geometry
326
+ }
327
+
328
+ type PreviewGeometry = {
329
+ fill: BufferGeometry | null
330
+ outline: BufferGeometry | null
331
+ corner: BufferGeometry | null
332
+ label: { position: [number, number, number]; values: number[] } | null
333
+ }
334
+
335
+ function buildPreviewGeometry(preview: WallRegionPreview, wall: WallNode): PreviewGeometry | null {
336
+ const mesh = sceneRegistry.nodes.get(wall.id) as Mesh | undefined
337
+ if (!mesh) return null
338
+ const nodes = useScene.getState().nodes
339
+ const frame = wallFaceFrame(wall, preview.face)
340
+ const other = wallFaceFrame(wall, preview.face === 'a' ? 'b' : 'a')
341
+ const extent = wallFaceExtent(wall, preview.face, mesh.geometry)
342
+ const runs = resolveWallFaceBase(wall, mesh.geometry, nodes)?.[preview.face] ?? null
343
+ const strips = (span: FaceSpan) => faceSpanStrips(frame, extent, runs, span)
344
+ const fillStrips = preview.bounds ? strips(preview.bounds) : []
345
+ // Where the box starts: the press point on hover, the pressed corner while dragging.
346
+ const [cu, cv] = preview.corner
347
+ const cornerStrips = strips({
348
+ u0: cu - CORNER_HALF_SIZE,
349
+ u1: cu + CORNER_HALF_SIZE,
350
+ v0: Math.max(0, cv - CORNER_HALF_SIZE),
351
+ v1: cv + CORNER_HALF_SIZE,
352
+ })
353
+ const { measure } = preview
354
+ return {
355
+ fill: fillStrips.length
356
+ ? positions(faceStripTriangles(frame, other, fillStrips, FILL_OFFSET))
357
+ : null,
358
+ outline: fillStrips.length
359
+ ? positions(faceStripOutline(frame, other, fillStrips, LINE_OFFSET))
360
+ : null,
361
+ corner: cornerStrips.length
362
+ ? positions(faceStripTriangles(frame, other, cornerStrips, LINE_OFFSET))
363
+ : null,
364
+ label: measure?.values.every((value) => value > 0.005)
365
+ ? {
366
+ position: facePoint(
367
+ frame,
368
+ other,
369
+ measure.u,
370
+ faceBaseAt(runs, measure.u) + measure.v,
371
+ 0.05,
372
+ ),
373
+ values: measure.values,
374
+ }
375
+ : null,
376
+ }
377
+ }
378
+
379
+ function WallRegionPreview3D() {
380
+ const preview = useWallPaintRegionSession((state) => state.preview)
381
+ const wall = useScene((state) => (preview ? state.nodes[preview.wallId as AnyNodeId] : undefined))
382
+ const tint = usePaintTint(DRAFT_COLOR)
383
+ const unit = useViewer((state) => state.unit)
384
+ const metricNotation = useViewer((state) => state.metricNotation)
385
+ const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark')
386
+ const root = useRef<Group>(null)
387
+ const geometry = useMemo(
388
+ () => (preview && wall?.type === 'wall' ? buildPreviewGeometry(preview, wall) : null),
389
+ [preview, wall],
390
+ )
391
+ useEffect(
392
+ () => () => {
393
+ geometry?.fill?.dispose()
394
+ geometry?.outline?.dispose()
395
+ geometry?.corner?.dispose()
396
+ },
397
+ [geometry],
398
+ )
399
+ useEffect(() => {
400
+ fillMaterial.color.set(tint)
401
+ }, [tint])
402
+ useFrame(() => {
403
+ const mesh = preview ? sceneRegistry.nodes.get(preview.wallId) : undefined
404
+ if (!root.current) return
405
+ root.current.visible = !!mesh
406
+ if (mesh) {
407
+ mesh.updateWorldMatrix(true, false)
408
+ root.current.matrix.copy(mesh.matrixWorld)
409
+ }
410
+ })
411
+ if (!geometry) return null
412
+ const label = geometry.label
413
+ const text = label
414
+ ? label.values.map((value) => formatLinearMeasurement(value, unit, metricNotation)).join(' × ')
415
+ : null
416
+ return (
417
+ <group matrixAutoUpdate={false} ref={root}>
418
+ {geometry.fill && (
419
+ <mesh
420
+ geometry={geometry.fill}
421
+ layers={EDITOR_LAYER}
422
+ material={fillMaterial}
423
+ raycast={noRaycast}
424
+ renderOrder={100}
425
+ />
426
+ )}
427
+ {geometry.outline && (
428
+ <lineSegments
429
+ geometry={geometry.outline}
430
+ layers={EDITOR_LAYER}
431
+ material={outlineMaterial}
432
+ raycast={noRaycast}
433
+ renderOrder={101}
434
+ />
435
+ )}
436
+ {geometry.corner && (
437
+ <mesh
438
+ geometry={geometry.corner}
439
+ layers={EDITOR_LAYER}
440
+ material={lineMaterial}
441
+ raycast={noRaycast}
442
+ renderOrder={102}
443
+ />
444
+ )}
445
+ {label && text && (
446
+ <DraftMeasurementLabel
447
+ color={isDark ? '#ffffff' : '#111111'}
448
+ label={text}
449
+ position={label.position}
450
+ shadowColor={isDark ? '#111111' : '#ffffff'}
451
+ />
452
+ )}
453
+ </group>
454
+ )
455
+ }
456
+
457
+ /**
458
+ * "Paint part of a surface" in 3D: a rectangle pressed anywhere on a wall face
459
+ * of the current level and dragged to its opposite corner, while the paint
460
+ * tool's Rectangle sub-mode is on.
461
+ */
462
+ export function WallPaintRegionTool() {
463
+ const { gl, camera } = useThree()
464
+ useWallRegionPointer(gl.domElement, camera)
465
+ return <WallRegionPreview3D />
466
+ }
@@ -13,6 +13,7 @@ import {
13
13
  createSceneApi,
14
14
  emitter,
15
15
  findLevelAncestorId,
16
+ floorPlacedCollides,
16
17
  footprintAABBFrom,
17
18
  type GridEvent,
18
19
  type GroupMoveSnapResult,
@@ -340,7 +341,8 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
340
341
  // position isn't in the level frame the spatial grid indexes. They may
341
342
  // still provide a parent-frame collision check and use the same bounds box.
342
343
  const collides =
343
- !frameParent && nodeRegistry.get(node.type)?.capabilities?.floorPlaced?.collides === true
344
+ !frameParent &&
345
+ floorPlacedCollides(nodeRegistry.get(node.type)?.capabilities?.floorPlaced, node)
344
346
  // Snapshot the scene once at drag-start — bounds depend on `node` (locked
345
347
  // for the lifetime of this tool) and any sibling state the kind reads. If a
346
348
  // future kind needs live sibling state mid-drag, switch to a subscribed
@@ -1,4 +1,4 @@
1
- import { sceneRegistry, useScene, type ZoneNode } from '@pascal-app/core'
1
+ import { sceneRegistry, useScene } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import { useThree } from '@react-three/fiber'
4
4
  import { useCallback, useEffect, useRef } from 'react'
@@ -248,18 +248,8 @@ function collectNodeIdsInScreenRect(
248
248
 
249
249
  function commitBoxSelection(ids: string[], event: PointerEvent) {
250
250
  const shouldAppend = event.metaKey || event.ctrlKey || event.shiftKey
251
- const { phase, structureLayer } = useEditor.getState()
252
251
  const viewer = useViewer.getState()
253
252
 
254
- if (phase === 'structure' && structureLayer === 'zones') {
255
- if (ids.length > 0) {
256
- viewer.setSelection({ zoneId: ids[0] as ZoneNode['id'] })
257
- } else if (!shouldAppend) {
258
- viewer.setSelection({ zoneId: null })
259
- }
260
- return
261
- }
262
-
263
253
  if (shouldAppend) {
264
254
  viewer.setSelection({
265
255
  selectedIds: Array.from(new Set([...viewer.selection.selectedIds, ...ids])),
@@ -274,7 +264,7 @@ export const BoxSelectTool: React.FC = () => {
274
264
  const phase = useEditor((s) => s.phase)
275
265
  const mode = useEditor((s) => s.mode)
276
266
  const selectionTool = useEditor((s) => s.floorplanSelectionTool)
277
- const isActive = mode === 'select' && (phase === 'structure' || phase === 'furnish')
267
+ const isActive = mode === 'select' && phase === 'building'
278
268
 
279
269
  if (!isActive) return null
280
270
 
@@ -0,0 +1,79 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { emitter, type GridEvent } from '@pascal-app/core'
3
+ import { useViewer } from '@pascal-app/viewer'
4
+ import { act } from '@react-three/fiber'
5
+ import { cancelActiveTool } from '../../../hooks/use-keyboard'
6
+ import { selectionEnabled } from '../../../lib/interaction/scope'
7
+ import useEditor from '../../../store/use-editor'
8
+ import useInteractionScope from '../../../store/use-interaction-scope'
9
+ import { withSelectionHarness } from '../../../test-utils/selection-harness'
10
+ import { PlaneBoxSelectTool } from './plane-box-select-tool'
11
+
12
+ for (const exit of ['Escape', 'unmount', 'pointercancel'] as const) {
13
+ test(`plane marquee resets its drag and scope on ${exit}`, async () => {
14
+ await withSelectionHarness(async ({ render, canvas }) => {
15
+ useEditor.setState({
16
+ phase: 'building',
17
+ mode: 'select',
18
+ floorplanSelectionTool: 'marquee',
19
+ isFloorplanHovered: true,
20
+ })
21
+ useViewer.setState({ inputDragging: false, cameraDragging: false })
22
+ useInteractionScope.getState().end()
23
+ await render(<PlaneBoxSelectTool />)
24
+ await act(async () => {
25
+ canvas.dispatchEvent(
26
+ Object.assign(new Event('pointerdown'), { button: 0, clientX: 200, clientY: 200 }),
27
+ )
28
+ emitter.emit('grid:move', {
29
+ position: [4, 0, 4],
30
+ nativeEvent: { clientX: 800, clientY: 800 },
31
+ } as GridEvent)
32
+ })
33
+ expect(useInteractionScope.getState().scope.kind).toBe('box-select')
34
+ if (exit === 'unmount') await render(null)
35
+ else
36
+ await act(async () => {
37
+ if (exit === 'Escape') cancelActiveTool()
38
+ else window.dispatchEvent(new Event('pointercancel'))
39
+ })
40
+ expect(selectionEnabled(useInteractionScope.getState().scope)).toBe(true)
41
+ expect(useViewer.getState().previewSelectedIds).toEqual([])
42
+ if (exit === 'Escape') expect(useEditor.getState().floorplanSelectionTool).toBe('marquee')
43
+ await act(async () => {
44
+ emitter.emit('grid:move', {
45
+ position: [6, 0, 6],
46
+ nativeEvent: { clientX: 900, clientY: 900 },
47
+ } as GridEvent)
48
+ canvas.dispatchEvent(
49
+ Object.assign(new Event('pointerup'), { button: 0, clientX: 900, clientY: 900 }),
50
+ )
51
+ })
52
+ expect(useInteractionScope.getState().scope.kind).toBe('idle')
53
+ })
54
+ })
55
+ }
56
+
57
+ test('plane marquee cleanup cannot release a replacement box-select owner', async () => {
58
+ await withSelectionHarness(async ({ render, canvas }) => {
59
+ useEditor.setState({ isFloorplanHovered: true })
60
+ useViewer.setState({ inputDragging: false, cameraDragging: false })
61
+ await render(<PlaneBoxSelectTool />)
62
+ await act(async () => {
63
+ canvas.dispatchEvent(
64
+ Object.assign(new Event('pointerdown'), { button: 0, clientX: 200, clientY: 200 }),
65
+ )
66
+ emitter.emit('grid:move', {
67
+ position: [4, 0, 4],
68
+ nativeEvent: { clientX: 800, clientY: 800 },
69
+ } as GridEvent)
70
+ })
71
+ const ownGesture = useInteractionScope.getState().gesture
72
+ useInteractionScope.getState().begin({ kind: 'box-select' })
73
+ const replacement = useInteractionScope.getState().gesture
74
+ expect(replacement).not.toBe(ownGesture)
75
+ await render(null)
76
+ expect(useInteractionScope.getState().scope.kind).toBe('box-select')
77
+ expect(useInteractionScope.getState().gesture).toBe(replacement)
78
+ })
79
+ })