@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
@@ -7,7 +7,6 @@ import {
7
7
  ItemNode,
8
8
  LevelNode,
9
9
  nodeRegistry,
10
- registerNode,
11
10
  type SurfacePlacement,
12
11
  sceneRegistry,
13
12
  useLiveTransforms,
@@ -17,6 +16,7 @@ import { Group, Vector3 } from 'three'
17
16
  import { commitFreshPlacementSubtree } from '../../../lib/fresh-planar-placement'
18
17
  import useEditor from '../../../store/use-editor'
19
18
  import useInteractionScope from '../../../store/use-interaction-scope'
19
+ import { registerHostingTestNode } from '../__fixtures__/hosting'
20
20
  import { createRegistryItemSurfaceMove } from './item-surface-move'
21
21
 
22
22
  const level = LevelNode.parse({ id: 'level_resolver' })
@@ -69,7 +69,7 @@ beforeEach(() => {
69
69
  savedRoots = useScene.getState().rootNodeIds
70
70
  restoreRegistry = nodeRegistry._snapshot()
71
71
  nodeRegistry._reset()
72
- registerNode({
72
+ registerHostingTestNode({
73
73
  kind: 'item',
74
74
  schemaVersion: 1,
75
75
  schema: ItemNode,
@@ -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,
@@ -42,7 +43,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
42
43
  import { type Group, Vector3 } from 'three'
43
44
  import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
44
45
  import { commitFreshPlacementSubtree } from '../../../lib/fresh-planar-placement'
45
- import { stripPlacementMetadataFlags } from '../../../lib/placement-metadata'
46
+ import {
47
+ isFreshPlacementMetadata,
48
+ stripPlacementMetadataFlags,
49
+ } from '../../../lib/placement-metadata'
46
50
  import {
47
51
  offsetPlanPositionByLocalCenter,
48
52
  resolvePrioritizedPlanarCursorPosition,
@@ -64,6 +68,7 @@ import useEditor, {
64
68
  isMagneticSnapActive,
65
69
  } from '../../../store/use-editor'
66
70
  import useFacingPose from '../../../store/use-facing-pose'
71
+ import useInteractionScope from '../../../store/use-interaction-scope'
67
72
  import { swallowNextClick } from '../../editor/node-arrow-handles'
68
73
  import { CursorSphere } from '../shared/cursor-sphere'
69
74
  import { DragBoundingBox } from '../shared/drag-bounding-box'
@@ -218,7 +223,14 @@ const ALIGNMENT_THRESHOLD_M = 0.08
218
223
  */
219
224
  type ClickTriggerEvent = GridEvent | NodeEvent<AnyNode>
220
225
 
221
- export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
226
+ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
227
+ const node = useMemo(
228
+ () =>
229
+ isFreshPlacementMetadata(source.metadata)
230
+ ? (useScene.getState().nodes[source.id] ?? source)
231
+ : source,
232
+ [source],
233
+ )
222
234
  const itemSurfaceMove = useMemo(() => createRegistryItemSurfaceMove(node), [node])
223
235
  const suppressRaycastsRef = useRef<(() => void) | null>(null)
224
236
  const previewGroupRef = useRef<Group>(null)
@@ -329,7 +341,8 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
329
341
  // position isn't in the level frame the spatial grid indexes. They may
330
342
  // still provide a parent-frame collision check and use the same bounds box.
331
343
  const collides =
332
- !frameParent && nodeRegistry.get(node.type)?.capabilities?.floorPlaced?.collides === true
344
+ !frameParent &&
345
+ floorPlacedCollides(nodeRegistry.get(node.type)?.capabilities?.floorPlaced, node)
333
346
  // Snapshot the scene once at drag-start — bounds depend on `node` (locked
334
347
  // for the lifetime of this tool) and any sibling state the kind reads. If a
335
348
  // future kind needs live sibling state mid-drag, switch to a subscribed
@@ -432,6 +445,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
432
445
  }, [])
433
446
 
434
447
  useEffect(() => {
448
+ const ownsSubtree = useInteractionScope.getState().adoptSubtree(node.id)
435
449
  useScene.temporal.getState().pause()
436
450
  const movementSfx = createMovementSfxTick()
437
451
  dragAnchorRef.current = null
@@ -1114,11 +1128,13 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
1114
1128
  } as Partial<AnyNode>
1115
1129
 
1116
1130
  if (isNew) {
1117
- const finalId = commitFreshPlacementSubtree(node.id as AnyNodeId, data)
1118
- if (finalId) {
1119
- committed = true
1120
- committedId = finalId
1121
- }
1131
+ const finalId = commitFreshPlacementSubtree(node.id as AnyNodeId, data, (reason) => {
1132
+ setSurfaceRejection(reason)
1133
+ setValid(false)
1134
+ })
1135
+ if (!finalId) return
1136
+ committed = true
1137
+ committedId = finalId
1122
1138
  } else {
1123
1139
  // Fold the connected-ductwork follow-updates into the SAME
1124
1140
  // batch as the moved node so the whole thing is one undo step.
@@ -1339,7 +1355,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
1339
1355
  clearParentFramePreview()
1340
1356
  if (isNew) {
1341
1357
  updateSurfaceNode(node.id, {}, null)
1342
- useScene.getState().deleteNode(node.id as AnyNodeId)
1358
+ if (!ownsSubtree) useScene.getState().deleteNode(node.id)
1343
1359
  } else {
1344
1360
  itemSurfaceMove?.restore()
1345
1361
  applyMeshPose(originalPosition, originalRotationY)
@@ -1376,7 +1392,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
1376
1392
  // unmount / commit paths uniformly.
1377
1393
  useAlignmentGuides.getState().clear()
1378
1394
  const finalisedBy2D = useEditor.getState().movingNodeOrigin === '2d'
1379
- if (!committed && !finalisedBy2D && (!isNew || itemSurfaceMove)) {
1395
+ if (!committed && !finalisedBy2D && (!isNew || (!ownsSubtree && itemSurfaceMove))) {
1380
1396
  useLiveTransforms.getState().clear(node.id)
1381
1397
  clearConnectivityOverrides()
1382
1398
  clearParentFramePreview()
@@ -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
+ })
@@ -1,14 +1,7 @@
1
1
  import '../../../three-types'
2
2
 
3
3
  import { Icon } from '@iconify/react'
4
- import {
5
- type AnyNodeId,
6
- emitter,
7
- type GridEvent,
8
- sceneRegistry,
9
- useScene,
10
- type ZoneNode,
11
- } from '@pascal-app/core'
4
+ import { type AnyNodeId, emitter, type GridEvent, sceneRegistry, useScene } from '@pascal-app/core'
12
5
  import { useViewer } from '@pascal-app/viewer'
13
6
  import { useThree } from '@react-three/fiber'
14
7
  import { useCallback, useEffect, useRef } from 'react'
@@ -26,9 +19,10 @@ import {
26
19
  Vector2,
27
20
  Vector3,
28
21
  } from 'three'
22
+ import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
29
23
  import { EDITOR_LAYER } from '../../../lib/constants'
30
24
  import { sfxEmitter } from '../../../lib/sfx-bus'
31
- import useEditor from '../../../store/use-editor'
25
+ import useInteractionScope from '../../../store/use-interaction-scope'
32
26
  import { CursorSphere } from '../shared/cursor-sphere'
33
27
  import { isBoxSelectPointerSuppressed, markBoxSelectHandled } from './box-select-state'
34
28
  import { collectSelectableCandidateIds } from './select-candidates'
@@ -297,6 +291,7 @@ export const PlaneBoxSelectTool: React.FC = () => {
297
291
  const currentPoint = useRef(new Vector3())
298
292
  const pointerDown = useRef(false)
299
293
  const isDragging = useRef(false)
294
+ const ownedGesture = useRef<object | null>(null)
300
295
  const startClientX = useRef(0)
301
296
  const startClientY = useRef(0)
302
297
  const gridY = useRef(0)
@@ -320,11 +315,36 @@ export const PlaneBoxSelectTool: React.FC = () => {
320
315
  const resetDrag = useCallback(() => {
321
316
  pointerDown.current = false
322
317
  isDragging.current = false
323
- rectFillRef.current.visible = false
318
+ const gesture = ownedGesture.current
319
+ if (gesture)
320
+ useInteractionScope.getState().endIf(() => useInteractionScope.getState().gesture === gesture)
321
+ ownedGesture.current = null
322
+ previousGridPosition.current = null
323
+ if (rectFillRef.current) rectFillRef.current.visible = false
324
324
  outlineRef.current.visible = false
325
325
  syncPreviewSelectedIds([])
326
326
  }, [syncPreviewSelectedIds])
327
327
 
328
+ useEffect(() => {
329
+ const onCancel = () => {
330
+ if (!pointerDown.current) return
331
+ if (
332
+ (!ownedGesture.current && useInteractionScope.getState().scope.kind === 'idle') ||
333
+ (ownedGesture.current && useInteractionScope.getState().gesture === ownedGesture.current)
334
+ ) {
335
+ markToolCancelConsumed()
336
+ }
337
+ resetDrag()
338
+ }
339
+ emitter.on('tool:cancel', onCancel)
340
+ window.addEventListener('pointercancel', resetDrag)
341
+ return () => {
342
+ emitter.off('tool:cancel', onCancel)
343
+ window.removeEventListener('pointercancel', resetDrag)
344
+ resetDrag()
345
+ }
346
+ }, [resetDrag])
347
+
328
348
  const raycastToGround = useCallback(
329
349
  (event: PointerEvent): Vector3 | null => {
330
350
  const rect = gl.domElement.getBoundingClientRect()
@@ -342,12 +362,11 @@ export const PlaneBoxSelectTool: React.FC = () => {
342
362
  useEffect(() => {
343
363
  const outline = outlineRef.current
344
364
  return () => {
345
- previewSelectedIdsRef.current = []
346
- setPreviewSelectedIds([])
365
+ resetDrag()
347
366
  outline.geometry.dispose()
348
367
  ;(outline.material as LineBasicMaterial).dispose()
349
368
  }
350
- }, [setPreviewSelectedIds])
369
+ }, [resetDrag])
351
370
 
352
371
  useEffect(() => {
353
372
  const unsubscribe = useViewer.subscribe((state) => {
@@ -440,15 +459,7 @@ export const PlaneBoxSelectTool: React.FC = () => {
440
459
 
441
460
  const ids = collectNodeIdsInPlaneBounds(bounds)
442
461
  const shouldAppend = event.metaKey || event.ctrlKey || event.shiftKey
443
- const { phase, structureLayer } = useEditor.getState()
444
-
445
- if (phase === 'structure' && structureLayer === 'zones') {
446
- if (ids.length > 0) {
447
- useViewer.getState().setSelection({ zoneId: ids[0] as ZoneNode['id'] })
448
- } else if (!shouldAppend) {
449
- useViewer.getState().setSelection({ zoneId: null })
450
- }
451
- } else if (shouldAppend) {
462
+ if (shouldAppend) {
452
463
  const currentIds = useViewer.getState().selection.selectedIds
453
464
  useViewer.getState().setSelection({
454
465
  selectedIds: Array.from(new Set([...currentIds, ...ids])),
@@ -507,6 +518,8 @@ export const PlaneBoxSelectTool: React.FC = () => {
507
518
  const dy = nativeEvent.clientY - startClientY.current
508
519
  if (!isDragging.current && Math.hypot(dx, dy) >= DRAG_THRESHOLD_PX) {
509
520
  isDragging.current = true
521
+ useInteractionScope.getState().begin({ kind: 'box-select' })
522
+ ownedGesture.current = useInteractionScope.getState().gesture
510
523
  }
511
524
 
512
525
  if (isDragging.current && rectFillRef.current && outlineRef.current) {
@@ -47,10 +47,7 @@ describe('selectable candidates', () => {
47
47
  },
48
48
  previewSelectedIds: [],
49
49
  })
50
- useEditor.setState({
51
- phase: 'structure',
52
- structureLayer: 'elements',
53
- })
50
+ useEditor.setState({ phase: 'building' })
54
51
  })
55
52
 
56
53
  test('includes building-scoped elevators for the active level building', () => {
@@ -106,4 +103,40 @@ describe('selectable candidates', () => {
106
103
 
107
104
  expect(collectSelectableCandidateIds()).toContain('elevator_test')
108
105
  })
106
+
107
+ test('Building box selection includes structural and furnishing kinds on the active level', () => {
108
+ for (const category of ['structure', 'furnish'] as const) {
109
+ const kind = `box-select-test-${category}`
110
+ if (!nodeRegistry.has(kind)) {
111
+ registerNode({
112
+ kind,
113
+ schemaVersion: 1,
114
+ schema: z.object({ type: z.literal(kind) }) as never,
115
+ category,
116
+ defaults: () => ({}),
117
+ capabilities: { selectable: {} },
118
+ } as AnyNodeDefinition)
119
+ }
120
+ }
121
+ useScene.setState({
122
+ nodes: {
123
+ level_test: {
124
+ id: 'level_test',
125
+ type: 'level',
126
+ parentId: 'building_test',
127
+ children: ['wall_pick', 'item_pick', 'item_hidden'],
128
+ },
129
+ wall_pick: { id: 'wall_pick', type: 'box-select-test-structure', parentId: 'level_test' },
130
+ item_pick: { id: 'item_pick', type: 'box-select-test-furnish', parentId: 'level_test' },
131
+ item_hidden: {
132
+ id: 'item_hidden',
133
+ type: 'box-select-test-furnish',
134
+ parentId: 'level_test',
135
+ visible: false,
136
+ },
137
+ item_other: { id: 'item_other', type: 'box-select-test-furnish', parentId: 'level_other' },
138
+ } as unknown as Record<string, AnyNode>,
139
+ })
140
+ expect(collectSelectableCandidateIds().sort()).toEqual(['item_pick', 'wall_pick'])
141
+ })
109
142
  })
@@ -19,17 +19,29 @@ function isVisibleSelectableNode(node: AnyNode): boolean {
19
19
  export function collectSelectableCandidateIds(): string[] {
20
20
  const { levelId } = useViewer.getState().selection
21
21
  const { nodes } = useScene.getState()
22
- const { phase, structureLayer } = useEditor.getState()
22
+ const { phase } = useEditor.getState()
23
23
  const result: string[] = []
24
24
  const seen = new Set<string>()
25
25
  const addNode = (node: AnyNode | undefined) => {
26
- if (!node || seen.has(node.id) || (node as { visible?: boolean }).visible === false) return
26
+ if (
27
+ !node ||
28
+ node.type === 'zone' ||
29
+ seen.has(node.id) ||
30
+ (node as { visible?: boolean }).visible === false
31
+ )
32
+ return
27
33
  seen.add(node.id)
28
34
  result.push(node.id)
29
35
  }
30
36
  const visitLevelDescendant = (id: AnyNodeId) => {
31
37
  const node = nodes[id]
32
- if (!node || seen.has(node.id) || (node as { visible?: boolean }).visible === false) return
38
+ if (
39
+ !node ||
40
+ node.type === 'zone' ||
41
+ seen.has(node.id) ||
42
+ (node as { visible?: boolean }).visible === false
43
+ )
44
+ return
33
45
 
34
46
  if (isRegistrySelectable(node.type)) {
35
47
  addNode(node)
@@ -52,14 +64,6 @@ export function collectSelectableCandidateIds(): string[] {
52
64
  const levelNode = nodes[levelId as AnyNodeId] as LevelNode | undefined
53
65
  if (levelNode?.type !== 'level') return []
54
66
 
55
- if (phase === 'structure' && structureLayer === 'zones') {
56
- for (const childId of levelNode.children) {
57
- const node = nodes[childId as AnyNodeId]
58
- if (node?.type === 'zone') addNode(node)
59
- }
60
- return result
61
- }
62
-
63
67
  for (const childId of levelNode.children) {
64
68
  visitLevelDescendant(childId as AnyNodeId)
65
69
  }
@@ -0,0 +1,24 @@
1
+ import { afterAll, afterEach, expect, spyOn, test } from 'bun:test'
2
+ import { type AnyNodeDefinition, nodeRegistry } from '@pascal-app/core'
3
+ import { getRegistryAffordanceTool, preloadRegistryAffordanceTools } from './affordance-dispatch'
4
+
5
+ const get = spyOn(nodeRegistry, 'get')
6
+ afterEach(() => get.mockReset())
7
+ afterAll(() => get.mockRestore())
8
+
9
+ test('a preloaded drag tool mounts directly without first-gesture suspense', async () => {
10
+ const Tool = () => null
11
+ const loader = async () => ({ default: Tool })
12
+ get.mockReturnValue({ affordanceTools: { move: loader } } as unknown as AnyNodeDefinition)
13
+ await preloadRegistryAffordanceTools('wall')
14
+ expect(getRegistryAffordanceTool('wall', 'move')).toBe(Tool)
15
+ expect(getRegistryAffordanceTool('wall', 'move')).toBe(Tool)
16
+ })
17
+
18
+ test('preloading does not change an already mounted lazy component identity', async () => {
19
+ const loader = async () => ({ default: () => null })
20
+ get.mockReturnValue({ affordanceTools: { move: loader } } as unknown as AnyNodeDefinition)
21
+ const mounted = getRegistryAffordanceTool('wall', 'move')
22
+ await preloadRegistryAffordanceTools('wall')
23
+ expect(getRegistryAffordanceTool('wall', 'move')).toBe(mounted)
24
+ })
@@ -15,6 +15,19 @@ import { type ComponentType, lazy } from 'react'
15
15
  */
16
16
  const lazyToolCache = new WeakMap<() => Promise<unknown>, ComponentType>()
17
17
 
18
+ export async function preloadRegistryAffordanceTools(kind: string): Promise<void> {
19
+ const tools = nodeRegistry.get(kind)?.affordanceTools
20
+ await Promise.allSettled(
21
+ Object.values(tools ?? {}).map(async (loader) => {
22
+ if (!loader || lazyToolCache.has(loader)) return
23
+ const module = (await loader()) as { default: ComponentType<any> }
24
+ // Keep an already-mounted lazy component stable. Otherwise use the resolved
25
+ // component directly so grabbing a preloaded handle never suspends its tool.
26
+ if (!lazyToolCache.has(loader)) lazyToolCache.set(loader, module.default)
27
+ }),
28
+ )
29
+ }
30
+
18
31
  export function getRegistryAffordanceTool(
19
32
  kind: string,
20
33
  affordance: string,
@@ -1,10 +1,11 @@
1
1
  import { Html } from '@react-three/drei'
2
2
  import type { ThreeElements } from '@react-three/fiber'
3
3
  import { forwardRef } from 'react'
4
- import type { Group } from 'three'
4
+ import { CircleGeometry, CylinderGeometry, type Group, SphereGeometry } from 'three'
5
5
  import { furnishTools } from '../../../components/ui/action-menu/furnish-tools'
6
6
  import { tools } from '../../../components/ui/action-menu/structure-tools'
7
7
  import { EDITOR_LAYER } from '../../../lib/constants'
8
+ import { wallCursorIcon } from '../../../lib/tool-cursor-icon'
8
9
  import useEditor from '../../../store/use-editor'
9
10
  import useWallSnapIndicator from '../../../store/use-wall-snap-indicator'
10
11
 
@@ -25,6 +26,19 @@ interface CursorSphereProps extends Omit<ThreeElements['group'], 'ref'> {
25
26
  tooltipContent?: React.ReactNode
26
27
  }
27
28
 
29
+ // One geometry each for the life of the module: the vertical line used to
30
+ // be a `<cylinderGeometry args={[…, height, 8]}>` rebuilt on EVERY height
31
+ // change (the cursor's drop to the floor moves with the pointer), and the
32
+ // ring / dot / tip were built and disposed on every toggle — WebGPU then
33
+ // drew a frame whose vertex buffers had just been destroyed ("Vertex
34
+ // buffer slot 0 required by [RenderPipeline "renderPipeline_MeshBasicMaterial_…"]
35
+ // was not set … DrawIndexed(96 …)" — the 8-segment cylinder's 96 indices).
36
+ // The line is a unit cylinder scaled to `height` instead.
37
+ const DOT_GEOMETRY = new CircleGeometry(0.06, 32)
38
+ const RING_GEOMETRY = new CircleGeometry(0.2, 32)
39
+ const LINE_GEOMETRY = new CylinderGeometry(0.01, 0.01, 1, 8)
40
+ const TIP_GEOMETRY = new SphereGeometry(0.08, 20, 14)
41
+
28
42
  export const CursorSphere = forwardRef<Group, CursorSphereProps>(function CursorSphere(
29
43
  {
30
44
  color = '#818cf8',
@@ -45,11 +59,14 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
45
59
  // (green square at a corner); hide the cursor's ground dot/ring so it
46
60
  // doesn't sit on top of that glyph.
47
61
  const isSnapping = useWallSnapIndicator((s) => s.point !== null)
62
+ const wallMode = useEditor((s) => s.continuationByContext.wall)
48
63
 
49
- // Find the icon for the current tool
50
- let activeToolConfig = null
64
+ // Find the icon for the current tool — the wall shows its Rooms variant.
65
+ let activeToolConfig: { label: string; iconSrc: string } | null | undefined = null
51
66
  if (mode === 'build' && tool) {
52
- if (tool === 'item' && catalogCategory) {
67
+ if (tool === 'wall') {
68
+ activeToolConfig = wallCursorIcon(wallMode)
69
+ } else if (tool === 'item' && catalogCategory) {
53
70
  activeToolConfig = furnishTools.find((t) => t.catalogCategory === catalogCategory)
54
71
  } else {
55
72
  activeToolConfig = tools.find((t) => t.id === tool)
@@ -67,8 +84,7 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
67
84
  <group rotation={[-Math.PI / 2, 0, 0]}>
68
85
  {/* Center dot — at the ground unless the placement point is elevated */}
69
86
  {!dotAtTip && (
70
- <mesh layers={EDITOR_LAYER} renderOrder={2}>
71
- <circleGeometry args={[0.06, 32]} />
87
+ <mesh geometry={DOT_GEOMETRY} layers={EDITOR_LAYER} renderOrder={2}>
72
88
  <meshBasicMaterial
73
89
  color={color}
74
90
  depthTest={false}
@@ -80,8 +96,7 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
80
96
  )}
81
97
 
82
98
  {/* Outer ring / glow */}
83
- <mesh layers={EDITOR_LAYER} renderOrder={2}>
84
- <circleGeometry args={[0.2, 32]} />
99
+ <mesh geometry={RING_GEOMETRY} layers={EDITOR_LAYER} renderOrder={2}>
85
100
  <meshBasicMaterial
86
101
  color={color}
87
102
  depthTest={false}
@@ -95,8 +110,13 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
95
110
 
96
111
  {/* Vertical line */}
97
112
  {height > 0 && (
98
- <mesh layers={EDITOR_LAYER} position={[0, height / 2, 0]} renderOrder={2}>
99
- <cylinderGeometry args={[0.01, 0.01, height, 8]} />
113
+ <mesh
114
+ geometry={LINE_GEOMETRY}
115
+ layers={EDITOR_LAYER}
116
+ position={[0, height / 2, 0]}
117
+ renderOrder={2}
118
+ scale={[1, height, 1]}
119
+ >
100
120
  <meshBasicMaterial
101
121
  color={color}
102
122
  depthTest={false}
@@ -110,8 +130,12 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
110
130
  {/* Bright marker dot at the tip of the line — the actual placement
111
131
  point, riding at the cursor while the line drops to the floor. */}
112
132
  {dotAtTip && height > 0 && (
113
- <mesh layers={EDITOR_LAYER} position={[0, height, 0]} renderOrder={2}>
114
- <sphereGeometry args={[0.08, 20, 14]} />
133
+ <mesh
134
+ geometry={TIP_GEOMETRY}
135
+ layers={EDITOR_LAYER}
136
+ position={[0, height, 0]}
137
+ renderOrder={2}
138
+ >
115
139
  <meshBasicMaterial color={color} depthTest={false} depthWrite={false} />
116
140
  </mesh>
117
141
  )}
@@ -0,0 +1,38 @@
1
+ 'use client'
2
+
3
+ import { Html } from '@react-three/drei'
4
+
5
+ /** Lift of a draft's readout above the top of what it measures, in metres. */
6
+ export const DRAFT_LABEL_Y_OFFSET = 0.22
7
+
8
+ /** Screen-space readout for a draft in 3D (lengths, angles, split segments). */
9
+ export function DraftMeasurementLabel({
10
+ color,
11
+ label,
12
+ position,
13
+ shadowColor,
14
+ }: {
15
+ color: string
16
+ label: string
17
+ position: [number, number, number]
18
+ shadowColor: string
19
+ }) {
20
+ return (
21
+ <Html
22
+ center
23
+ position={position}
24
+ style={{ pointerEvents: 'none', userSelect: 'none' }}
25
+ zIndexRange={[100, 0]}
26
+ >
27
+ <div
28
+ className="whitespace-nowrap font-bold font-mono text-[15px]"
29
+ style={{
30
+ color,
31
+ textShadow: `-1.5px -1.5px 0 ${shadowColor}, 1.5px -1.5px 0 ${shadowColor}, -1.5px 1.5px 0 ${shadowColor}, 1.5px 1.5px 0 ${shadowColor}, 0 0 4px ${shadowColor}, 0 0 4px ${shadowColor}`,
32
+ }}
33
+ >
34
+ {label}
35
+ </div>
36
+ </Html>
37
+ )
38
+ }