@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
@@ -56,6 +56,8 @@ export function useCeilingEvents() {
56
56
  const hoveredRef = useRef<string | null>(null)
57
57
 
58
58
  useEffect(() => {
59
+ // XR placement is driven by the controller-ray bridge, never screen coordinates.
60
+ if (gl.xr.isPresenting) return
59
61
  const canvas = gl.domElement
60
62
 
61
63
  const isActive = (): boolean => {
@@ -70,6 +70,7 @@ test('grid moves throttle camera drags at 100 ms and resume immediately during t
70
70
  render() {},
71
71
  setSize() {},
72
72
  setPixelRatio() {},
73
+ xr: { isPresenting: false },
73
74
  } as unknown as WebGLRenderer,
74
75
  camera,
75
76
  frameloop: 'never',
@@ -199,6 +200,7 @@ test.each([
199
200
  render() {},
200
201
  setSize() {},
201
202
  setPixelRatio() {},
203
+ xr: { isPresenting: false },
202
204
  } as unknown as WebGLRenderer,
203
205
  camera,
204
206
  frameloop: 'never',
@@ -3,6 +3,7 @@ import {
3
3
  type EventSuffix,
4
4
  emitter,
5
5
  type GridEvent,
6
+ hiddenWallPointerEventsHeld,
6
7
  nodeRegistry,
7
8
  sceneRegistry,
8
9
  useScene,
@@ -58,6 +59,8 @@ export function useGridEvents(gridY: number) {
58
59
  }, [gridY])
59
60
 
60
61
  useEffect(() => {
62
+ // XR placement is driven by the controller-ray bridge, never screen coordinates.
63
+ if (gl.xr.isPresenting) return
61
64
  const canvas = gl.domElement
62
65
  setSurfaceRaycastLayers(raycaster.current.layers)
63
66
 
@@ -132,7 +135,10 @@ export function useGridEvents(gridY: number) {
132
135
  // Architectural meshes are expensive to raycast and are meaningful only
133
136
  // to an active placement/drafting interaction. Floor tools retain the
134
137
  // ordinary terrain/grid intersection without scanning every wall.
135
- const surfaceHit = semanticSurfaceQueryRef.current ? getSurfaceIntersection() : null
138
+ const surfaceHit =
139
+ semanticSurfaceQueryRef.current || hiddenWallPointerEventsHeld()
140
+ ? getSurfaceIntersection()
141
+ : null
136
142
 
137
143
  // A semantic architectural hit is the authoritative cursor position.
138
144
  // Do not replace it with the terrain/grid intersection below: that would
@@ -244,16 +250,15 @@ export function useGridEvents(gridY: number) {
244
250
  : undefined,
245
251
  surfaceNormal: localNormal ? [localNormal.x, localNormal.y, localNormal.z] : undefined,
246
252
  surfaceObject: point.surface?.object,
247
- surfaceHit:
248
- semanticSurfaceQueryRef.current && point.surface
249
- ? {
250
- kind: point.surface.descriptor.kind,
251
- hostId: point.surface.hostId,
252
- face: classifiedFace?.face ?? 'unknown',
253
- levelId: useViewer.getState().selection.levelId ?? undefined,
254
- side: classifiedFace?.side,
255
- }
256
- : undefined,
253
+ surfaceHit: point.surface
254
+ ? {
255
+ kind: point.surface.descriptor.kind,
256
+ hostId: point.surface.hostId,
257
+ face: classifiedFace?.face ?? 'unknown',
258
+ levelId: useViewer.getState().selection.levelId ?? undefined,
259
+ side: classifiedFace?.side,
260
+ }
261
+ : undefined,
257
262
  nativeEvent: nativeEvent as any, // Type compatibility with ThreeEvent
258
263
  }
259
264
 
@@ -10,20 +10,29 @@ import {
10
10
  nodeRegistry,
11
11
  PipeFittingNode,
12
12
  PipeSegmentNode,
13
+ registerNode,
14
+ useInteractive,
13
15
  useScene,
14
16
  } from '@pascal-app/core'
17
+ import { useSidebarStore } from '../components/ui/primitives/sidebar'
15
18
  import { runRedo, runUndo } from '../lib/history'
16
19
  import { meshEditScope } from '../lib/interaction/scope'
20
+ import { setSidebarTabIds } from '../lib/sidebar-panel'
17
21
  import useEditor from '../store/use-editor'
18
22
  import useInteractionScope from '../store/use-interaction-scope'
19
23
  import {
24
+ armPaintFromShortcut,
25
+ armWallToolFromShortcut,
20
26
  blocksSnappingShortcut,
21
27
  canCycleSnappingModeShortcut,
22
28
  canRunGlobalRotationShortcut,
23
29
  isToolOwnedCanopyForm,
24
30
  isToolOwnedRotation,
25
31
  markToolCancelConsumed,
32
+ openItemCatalogFromShortcut,
26
33
  runHistoryShortcut,
34
+ runNodeInteraction,
35
+ switchPhaseFromShortcut,
27
36
  } from './use-keyboard'
28
37
 
29
38
  type RafFn = (callback: (time: number) => void) => number
@@ -106,6 +115,12 @@ describe('rotation shortcut ownership', () => {
106
115
  expect(isToolOwnedRotation()).toBe(true)
107
116
  })
108
117
 
118
+ test('does not reserve R for the wall tool: its shape is picked in the Build panel', () => {
119
+ useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
120
+
121
+ expect(isToolOwnedRotation()).toBe(false)
122
+ })
123
+
109
124
  test('leaves R and T to a moving lean-to extension', () => {
110
125
  const leanTo = { id: 'lean_to_moving', type: 'lean-to-extension' } as unknown as AnyNode
111
126
  useInteractionScope.getState().begin({
@@ -242,3 +257,170 @@ describe('history while drawing distribution runs', () => {
242
257
  })
243
258
  }
244
259
  })
260
+
261
+ describe('E on a single selected node', () => {
262
+ function registerKind(overrides: Record<string, unknown>) {
263
+ const kind = `test:e-${crypto.randomUUID()}`
264
+ registerNode({
265
+ kind,
266
+ schemaVersion: 1,
267
+ category: 'furnish',
268
+ defaults: () => ({}),
269
+ capabilities: {},
270
+ ...overrides,
271
+ } as never)
272
+ return { object: 'node', id: `${kind}_1`, type: kind } as unknown as AnyNode
273
+ }
274
+
275
+ const switchMechanism = {
276
+ has: () => true,
277
+ isOn: (node: AnyNode, state: ReturnType<typeof useInteractive.getState>) =>
278
+ Boolean(state.mechanisms[node.id]),
279
+ set: (node: AnyNode, on: boolean) => useInteractive.getState().setMechanism(node.id, on),
280
+ }
281
+
282
+ test('runs the mechanism of a kind that declares only the capability', () => {
283
+ const node = registerKind({ capabilities: { mechanism: switchMechanism } })
284
+ try {
285
+ expect(runNodeInteraction(node)).toBe(true)
286
+ expect(useInteractive.getState().mechanisms[node.id]).toBe(true)
287
+ expect(runNodeInteraction(node)).toBe(true)
288
+ expect(useInteractive.getState().mechanisms[node.id]).toBe(false)
289
+ } finally {
290
+ useInteractive.getState().removeMechanism(node.id)
291
+ }
292
+ })
293
+
294
+ test("prefers the kind's own E action over its mechanism", () => {
295
+ let ran = 0
296
+ const node = registerKind({
297
+ capabilities: { mechanism: switchMechanism },
298
+ keyboardActions: { e: { appliesTo: () => true, run: () => (ran += 1) } },
299
+ })
300
+ expect(runNodeInteraction(node)).toBe(true)
301
+ expect(ran).toBe(1)
302
+ expect(useInteractive.getState().mechanisms[node.id]).toBeUndefined()
303
+ })
304
+
305
+ test('leaves E to the door and window fallbacks when nothing applies', () => {
306
+ const node = registerKind({
307
+ capabilities: { mechanism: { ...switchMechanism, has: () => false } },
308
+ })
309
+ expect(runNodeInteraction(node)).toBe(false)
310
+ expect(runNodeInteraction(registerKind({}))).toBe(false)
311
+ })
312
+ })
313
+
314
+ describe('B and P open their panel', () => {
315
+ afterEach(() => {
316
+ setSidebarTabIds([])
317
+ useSidebarStore.getState().setIsCollapsed(false)
318
+ useEditor.getState().setContinuation('wall', 'room')
319
+ useEditor.getState().armToolMode({ mode: 'select' })
320
+ })
321
+
322
+ test('1 goes to site and 2 into the building; 3 is no longer a shortcut', () => {
323
+ useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
324
+ expect(switchPhaseFromShortcut('1')).toBe(true)
325
+ expect(useEditor.getState().phase).toBe('site')
326
+ expect(useEditor.getState().mode).toBe('select')
327
+ expect(switchPhaseFromShortcut('2')).toBe(true)
328
+ expect(useEditor.getState().phase).toBe('building')
329
+ expect(switchPhaseFromShortcut('3')).toBe(false)
330
+ expect(useEditor.getState().phase).toBe('building')
331
+ })
332
+
333
+ test('F, B and P arm their tool and keep the building', () => {
334
+ setSidebarTabIds(['site', 'build', 'paint', 'items'])
335
+ useEditor.getState().setPhase('building')
336
+ for (const [arm, mode, tool] of [
337
+ [openItemCatalogFromShortcut, 'build', 'item'],
338
+ [armWallToolFromShortcut, 'build', 'wall'],
339
+ [armPaintFromShortcut, 'material-paint', null],
340
+ ] as const) {
341
+ arm()
342
+ expect(useEditor.getState().phase).toBe('building')
343
+ expect(useEditor.getState().mode).toBe(mode)
344
+ expect(useEditor.getState().tool).toBe(tool)
345
+ }
346
+ openItemCatalogFromShortcut()
347
+ expect(useEditor.getState().activeSidebarPanel).toBe('items')
348
+ })
349
+
350
+ test('F, B and P also keep Site when arming their tool', () => {
351
+ setSidebarTabIds(['site', 'build', 'paint', 'items'])
352
+ for (const arm of [
353
+ openItemCatalogFromShortcut,
354
+ armWallToolFromShortcut,
355
+ armPaintFromShortcut,
356
+ ]) {
357
+ useEditor.getState().setPhase('site')
358
+ useEditor.getState().armToolMode({ mode: 'select' })
359
+ arm()
360
+ expect(useEditor.getState().phase).toBe('site')
361
+ }
362
+ })
363
+
364
+ test('F opens a collapsed catalog sidebar', () => {
365
+ setSidebarTabIds(['site', 'build', 'items'])
366
+ useSidebarStore.getState().setIsCollapsed(true)
367
+ openItemCatalogFromShortcut()
368
+ expect(useEditor.getState().activeSidebarPanel).toBe('items')
369
+ expect(useSidebarStore.getState().isCollapsed).toBe(false)
370
+ })
371
+
372
+ test('B arms the wall tool on Rectangle and shows the Build panel', () => {
373
+ setSidebarTabIds(['site', 'build', 'paint', 'items'])
374
+ useEditor.getState().setActiveSidebarPanel('items')
375
+ useSidebarStore.getState().setIsCollapsed(true)
376
+ useEditor.getState().setContinuation('wall', 'single')
377
+
378
+ armWallToolFromShortcut()
379
+
380
+ const editor = useEditor.getState()
381
+ expect(editor.mode).toBe('build')
382
+ expect(editor.tool).toBe('wall')
383
+ expect(editor.getContinuation('wall')).toBe('rectangle')
384
+ expect(editor.activeSidebarPanel).toBe('build')
385
+ expect(useSidebarStore.getState().isCollapsed).toBe(false)
386
+ })
387
+
388
+ // A host may drop the armed tool when the panel changes (the community does,
389
+ // with any placement in flight), so B and F open their panel first.
390
+ test('B and F keep their tool from another panel where the host disarms on a panel change', () => {
391
+ setSidebarTabIds(['build', 'paint', 'items', 'ai'])
392
+ const unsubscribe = useEditor.subscribe((state, prev) => {
393
+ if (state.activeSidebarPanel !== prev.activeSidebarPanel) {
394
+ useEditor.getState().armToolMode({ mode: 'select' })
395
+ }
396
+ })
397
+ try {
398
+ for (const [arm, from, panel, tool] of [
399
+ [armWallToolFromShortcut, 'ai', 'build', 'wall'],
400
+ [openItemCatalogFromShortcut, 'paint', 'items', 'item'],
401
+ ] as const) {
402
+ useEditor.getState().setActiveSidebarPanel(from)
403
+ arm()
404
+ const editor = useEditor.getState()
405
+ expect(editor.activeSidebarPanel).toBe(panel)
406
+ expect(editor.mode).toBe('build')
407
+ expect(editor.tool).toBe(tool)
408
+ }
409
+ } finally {
410
+ unsubscribe()
411
+ }
412
+ })
413
+
414
+ test('P arms paint and shows the Paint panel, or Build where the host has none', () => {
415
+ setSidebarTabIds(['build', 'paint', 'items'])
416
+ useEditor.getState().setActiveSidebarPanel('items')
417
+ armPaintFromShortcut()
418
+ expect(useEditor.getState().mode).toBe('material-paint')
419
+ expect(useEditor.getState().activeSidebarPanel).toBe('paint')
420
+
421
+ setSidebarTabIds(['site', 'build', 'items'])
422
+ useEditor.getState().setActiveSidebarPanel('items')
423
+ armPaintFromShortcut()
424
+ expect(useEditor.getState().activeSidebarPanel).toBe('build')
425
+ })
426
+ })
@@ -5,11 +5,11 @@ import {
5
5
  nodeRegistry,
6
6
  pauseSpaceDetection,
7
7
  resumeSpaceDetection,
8
+ toggleNodeMechanism,
8
9
  useScene,
9
10
  } from '@pascal-app/core'
10
11
  import { cancelPerfAction, markPerfAction, useViewer } from '@pascal-app/viewer'
11
12
  import { useEffect } from 'react'
12
- import { Vector3 } from 'three'
13
13
  import {
14
14
  cutSelectionToEditorClipboard,
15
15
  deleteSelection,
@@ -18,25 +18,32 @@ import {
18
18
  import {
19
19
  classifyParticipant,
20
20
  collectParticipants,
21
- computeGroupBox,
22
- expandToComponent,
21
+ groupPlanBounds,
23
22
  levelFrame,
23
+ planBoundsCenter,
24
24
  rotateGroupPatches,
25
25
  } from '../components/editor/group-transform-shared'
26
26
  import { steppedRotation } from '../components/tools/item/placement-math'
27
+ import { keyCyclableContinuationContext } from '../lib/continuation'
27
28
  import { resolveDirectManipulationNode } from '../lib/direct-manipulation'
28
- import { toggleDoorOpenState } from '../lib/door-interaction'
29
+ import { cancelGestures } from '../lib/gesture-lifecycle'
29
30
  import { guideEmitter } from '../lib/guide-events'
30
31
  import { isHistoryShortcut, runRedo, runUndo, shouldCancelDraftOnHistoryJump } from '../lib/history'
31
32
  import { isActive } from '../lib/interaction/scope'
33
+ import { paintRegionModeActive } from '../lib/paint-region-mode'
34
+ import { popRoomSelection } from '../lib/room-selection-commands'
32
35
  import { copySelectedNodesToEditorClipboard } from '../lib/scene-clipboard'
33
36
  import { sfxEmitter } from '../lib/sfx-bus'
37
+ import { openSidebarPanel } from '../lib/sidebar-panel'
34
38
  import { activeSiteNode, clampBrushRadius } from '../lib/terrain-sculpt'
35
39
  import { leaveUnitFocus } from '../lib/units'
36
- import { toggleWindowOpenState } from '../lib/window-interaction'
40
+ import { selectWallDrawVariant } from '../lib/wall-draw-variant'
37
41
  import useDeleteConfirmation from '../store/use-delete-confirmation'
38
42
  import useEditor, { getActiveContinuationContext, getActiveSnapContext } from '../store/use-editor'
39
- import useInteractionScope, { getMovingNode } from '../store/use-interaction-scope'
43
+ import useInteractionScope, {
44
+ getMovingNode,
45
+ isInteractionSubtreeDraft,
46
+ } from '../store/use-interaction-scope'
40
47
  import { groupCurrentSelection, ungroupCurrentSelection } from '../store/use-session-groups'
41
48
 
42
49
  // References (guide/scan) are selected via `useEditor.selectedReferenceId`, not
@@ -66,27 +73,20 @@ function rotateGroupSelection(direction: 1 | -1): boolean {
66
73
  (id) => classifyParticipant(nodes[id as AnyNodeId], levelId, nodes) !== null,
67
74
  )
68
75
  if (participantIds.length === 0) return false
69
- const fullIds = expandToComponent(participantIds, nodes, levelId)
70
- const { starts, links } = collectParticipants(fullIds, nodes, levelId)
76
+ const { starts, links } = collectParticipants(participantIds, nodes, levelId)
71
77
  if (starts.length === 0) return false
72
78
 
73
- // Same pivot as the 3D gizmo: the selection's world bbox center, converted
74
- // into the level frame before orbiting placements (a rotated building would
75
- // otherwise displace the centre).
76
- const box = computeGroupBox(fullIds)
77
- if (!box) return false
78
- const worldCenter = new Vector3(
79
- (box.min.x + box.max.x) / 2,
80
- box.min.y,
81
- (box.min.z + box.max.z) / 2,
82
- )
83
- const localCenter = worldCenter.applyMatrix4(levelFrame(levelId).inverse)
79
+ // Same pivot as the dashed boxes and the rotate gizmo: the selection's box
80
+ // centre in the level frame.
81
+ const bounds = groupPlanBounds(starts, levelFrame(levelId).inverse)
82
+ if (!bounds) return false
83
+ const [pivotX, pivotZ] = planBoundsCenter(bounds)
84
84
 
85
85
  // R (+45° yaw) orbits by -45° in the atan2 x→z sense: yaw = rotation - delta
86
86
  // (see rotateGroupPatches), so keyboard direction matches the single-node
87
87
  // steppedRotation sense.
88
88
  const delta = -direction * (Math.PI / 4)
89
- const patches = rotateGroupPatches(starts, links, { x: localCenter.x, z: localCenter.z }, delta)
89
+ const patches = rotateGroupPatches(starts, links, { x: pivotX, z: pivotZ }, delta)
90
90
  // Space detection stays out: a rigid rotation of existing walls must not
91
91
  // re-create the room's auto floors/ceilings at the new bearing.
92
92
  pauseSpaceDetection()
@@ -113,19 +113,23 @@ export const markToolCancelConsumed = () => {
113
113
  // preset/item placement rely on this — they pass no coordinator onCancel, and
114
114
  // it is the mode switch unmounting them that destroys the draft.
115
115
  const exitToSelectAfterUnconsumedCancel = () => {
116
- const currentPhase = useEditor.getState().phase
117
- const currentStructureLayer = useEditor.getState().structureLayer
118
-
119
- useInteractionScope.getState().endIf((sc) => sc.kind === 'reshaping' && sc.reshape === 'hole')
120
-
121
- // From zone mode, return to structure select
122
- if (currentPhase === 'structure' && currentStructureLayer === 'zones') {
123
- useEditor.getState().setStructureLayer('elements')
124
- useEditor.getState().armToolMode({ mode: 'select' })
125
- } else {
126
- // Return to the default select tool while keeping the active building/level context.
127
- useEditor.getState().armToolMode({ mode: 'select' })
128
- }
116
+ const editor = useEditor.getState()
117
+ if (
118
+ editor.mode === 'select' &&
119
+ useInteractionScope.getState().scope.kind === 'idle' &&
120
+ popRoomSelection()
121
+ )
122
+ return
123
+ useInteractionScope
124
+ .getState()
125
+ .endIf(
126
+ (sc) =>
127
+ (sc.kind === 'placing' && isInteractionSubtreeDraft()) ||
128
+ (sc.kind === 'reshaping' && sc.reshape === 'hole'),
129
+ )
130
+
131
+ // Back to the select tool, keeping the active building/level context.
132
+ useEditor.getState().armToolMode({ mode: 'select' })
129
133
 
130
134
  useEditor.getState().setFloorplanSelectionTool('click')
131
135
 
@@ -134,6 +138,20 @@ const exitToSelectAfterUnconsumedCancel = () => {
134
138
  useEditor.getState().setSelectedReferenceId(null)
135
139
  }
136
140
 
141
+ // Cancel the active editor action with the same consume-or-exit semantics as
142
+ // Escape. Spatial inputs use this instead of unconditionally selecting the
143
+ // Select tool, so multi-step tools can keep their tool active after clearing
144
+ // the current draft.
145
+ export const cancelActiveTool = () => {
146
+ _toolCancelConsumed = false
147
+ emitter.emit('tool:cancel')
148
+ if (!_toolCancelConsumed) {
149
+ if (leaveUnitFocus()) useEditor.getState().armToolMode({ mode: 'select' })
150
+ else exitToSelectAfterUnconsumedCancel()
151
+ }
152
+ return _toolCancelConsumed
153
+ }
154
+
137
155
  // ⌘Z pressed mid-interaction (moving a node, drawing a wall, mid-placement…)
138
156
  // reads as "abort this action", not history undo — behave exactly like Escape
139
157
  // and report whether anything was in flight so the undo/redo arms know to
@@ -145,6 +163,11 @@ const cancelInteractionForHistoryShortcut = () => {
145
163
  guideEmitter.emit('guide:cancel-reference-scale')
146
164
  return true
147
165
  }
166
+ // A live gesture reads ⌘Z as "abort", never as a jump under the pointer.
167
+ if (cancelGestures('history')) {
168
+ cancelPerfAction()
169
+ return true
170
+ }
148
171
  const activeScope = useInteractionScope.getState().scope
149
172
  if (shouldCancelDraftOnHistoryJump()) return false
150
173
  if (activeScope.kind === 'mesh-editing' && activeScope.phase === 'selecting') return false
@@ -176,6 +199,41 @@ export const runHistoryShortcut = (direction: 'undo' | 'redo') => {
176
199
  return true
177
200
  }
178
201
 
202
+ /**
203
+ * B: the wall tool on Rectangle — the quickest room — with the Build panel
204
+ * showing, so the lit tile says what is in hand. The panel's tiles still pick
205
+ * another shape for the session. The panel opens before the tool is armed: a
206
+ * host may drop the armed tool when the panel changes.
207
+ */
208
+ export const armWallToolFromShortcut = () => {
209
+ selectWallDrawVariant('rectangle')
210
+ openSidebarPanel(['build'])
211
+ useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
212
+ }
213
+
214
+ /** 1: site, 2: the building — each back on the select tool. Returns whether `key` was one. */
215
+ export const switchPhaseFromShortcut = (key: string): boolean => {
216
+ const phase = key === '1' ? 'site' : key === '2' ? 'building' : null
217
+ if (!phase) return false
218
+ useEditor.getState().setPhase(phase)
219
+ useEditor.getState().armToolMode({ mode: 'select' })
220
+ return true
221
+ }
222
+
223
+ /** F: the item tool with the items catalog showing, opened first as B's is. */
224
+ export const openItemCatalogFromShortcut = () => {
225
+ openSidebarPanel(['items'])
226
+ useEditor.getState().setActiveSidebarPanel('items')
227
+ useEditor.getState().armToolMode({ mode: 'build', tool: 'item' })
228
+ }
229
+
230
+ /** P: paint mode, with the Paint panel showing (the Build panel where a host has no Paint tab). */
231
+ export const armPaintFromShortcut = () => {
232
+ useEditor.getState().armMaterialPaint()
233
+ openSidebarPanel(['paint', 'build'])
234
+ }
235
+
236
+ /** Whether an armed tool owns the rotation keys (`R` / `T`) instead of the selection. */
179
237
  export const isToolOwnedRotation = () => {
180
238
  const editor = useEditor.getState()
181
239
  const moving = getMovingNode()
@@ -219,6 +277,16 @@ export function blocksSnappingShortcut(
219
277
  return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable
220
278
  }
221
279
 
280
+ /** E on one selected node: the kind's own E action, else its mechanism. False when neither applies. */
281
+ export function runNodeInteraction(node: AnyNode): boolean {
282
+ const action = nodeRegistry.get(node.type)?.keyboardActions?.e
283
+ if (action?.appliesTo(node)) {
284
+ action.run(node)
285
+ return true
286
+ }
287
+ return toggleNodeMechanism(node)
288
+ }
289
+
222
290
  export const useKeyboard = ({
223
291
  isVersionPreviewMode = false,
224
292
  disabled = false,
@@ -290,7 +358,12 @@ export const useKeyboard = ({
290
358
  return
291
359
  }
292
360
 
293
- if (e.key === 'Shift' && !e.repeat && useEditor.getState().mode === 'material-paint') {
361
+ if (
362
+ e.key === 'Shift' &&
363
+ !e.repeat &&
364
+ useEditor.getState().mode === 'material-paint' &&
365
+ !paintRegionModeActive('material-paint')
366
+ ) {
294
367
  // In paint mode Shift cycles the application scope (this surface →
295
368
  // whole item / all matching / room) — the paint-mode analogue of the
296
369
  // snapping-mode cycle below. The scope chip mirrors this key.
@@ -342,7 +415,10 @@ export const useKeyboard = ({
342
415
  const current = useEditor.getState().getContinuation('fence')
343
416
  useEditor
344
417
  .getState()
345
- .setContinuation('fence', current === 'curved' ? 'continuous' : 'curved')
418
+ .setContinuation(
419
+ 'fence',
420
+ current === 'curved' ? 'freehand' : current === 'freehand' ? 'continuous' : 'curved',
421
+ )
346
422
  sfxEmitter.emit('sfx:grid-snap')
347
423
  return
348
424
  }
@@ -356,12 +432,13 @@ export const useKeyboard = ({
356
432
  !e.shiftKey &&
357
433
  !e.altKey
358
434
  ) {
359
- const context = getActiveContinuationContext()
435
+ // The wall's drawing mode is picked in the Build panel, not cycled here.
436
+ const context = keyCyclableContinuationContext(getActiveContinuationContext())
360
437
  if (context) {
361
438
  e.preventDefault()
362
439
  if (context === 'fence') {
363
440
  const current = useEditor.getState().getContinuation('fence')
364
- if (current !== 'curved') {
441
+ if (current !== 'curved' && current !== 'freehand') {
365
442
  useEditor
366
443
  .getState()
367
444
  .setContinuation('fence', current === 'single' ? 'continuous' : 'single')
@@ -385,46 +462,24 @@ export const useKeyboard = ({
385
462
  return
386
463
  }
387
464
 
388
- _toolCancelConsumed = false
389
- emitter.emit('tool:cancel')
390
-
391
465
  // Only switch to select mode if no tool had an active mid-action to cancel.
392
466
  // (e.g. mid-wall draw or mid-slab polygon should only cancel the action, not exit the tool)
393
- if (!_toolCancelConsumed) {
394
- if (leaveUnitFocus()) useEditor.getState().armToolMode({ mode: 'select' })
395
- else exitToSelectAfterUnconsumedCancel()
396
- }
397
- } else if (e.key === '1' && !e.metaKey && !e.ctrlKey) {
398
- e.preventDefault()
399
- useEditor.getState().setPhase('site')
400
- useEditor.getState().armToolMode({ mode: 'select' })
401
- } else if (e.key === '2' && !e.metaKey && !e.ctrlKey) {
402
- e.preventDefault()
403
- useEditor.getState().setPhase('structure')
404
- useEditor.getState().armToolMode({ mode: 'select' })
405
- } else if (e.key === '3' && !e.metaKey && !e.ctrlKey) {
467
+ cancelActiveTool()
468
+ } else if (!e.metaKey && !e.ctrlKey && switchPhaseFromShortcut(e.key)) {
406
469
  e.preventDefault()
407
- useEditor.getState().setPhase('furnish')
408
- useEditor.getState().armToolMode({ mode: 'select' })
409
470
  } else if (e.key === 'f' && !e.metaKey && !e.ctrlKey) {
410
471
  if (isVersionPreviewMode) return
411
472
  if (isToolOwnedCanopyForm()) return
412
473
  e.preventDefault()
413
- useEditor.getState().setPhase('furnish')
414
- useEditor.getState().armToolMode({ mode: 'build', tool: 'item' })
415
- useEditor.getState().setActiveSidebarPanel('items')
474
+ openItemCatalogFromShortcut()
416
475
  } else if (e.key === 'z' && !e.metaKey && !e.ctrlKey) {
417
476
  if (isVersionPreviewMode) return
418
477
  e.preventDefault()
419
- useEditor.getState().setPhase('structure')
420
- useEditor.getState().setStructureLayer('zones')
421
478
  useEditor.getState().armToolMode({ mode: 'build', tool: 'zone' })
422
479
  } else if (e.key === 'm' && !e.metaKey && !e.ctrlKey) {
423
480
  if (isVersionPreviewMode) return
424
481
  e.preventDefault()
425
482
  const editor = useEditor.getState()
426
- editor.setPhase('structure')
427
- editor.setStructureLayer('elements')
428
483
  editor.setToolDefaults('measurement', { kind: editor.lastMeasurementKind })
429
484
  editor.armToolMode({ mode: 'build', tool: 'measurement' })
430
485
  }
@@ -435,9 +490,7 @@ export const useKeyboard = ({
435
490
  } else if (e.key === 'b' && !e.metaKey && !e.ctrlKey) {
436
491
  if (isVersionPreviewMode) return
437
492
  e.preventDefault()
438
- useEditor.getState().setPhase('structure')
439
- useEditor.getState().setStructureLayer('elements')
440
- useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
493
+ armWallToolFromShortcut()
441
494
  } else if (e.key === 'x' && !e.metaKey && !e.ctrlKey) {
442
495
  if (isVersionPreviewMode) return
443
496
  e.preventDefault()
@@ -445,9 +498,7 @@ export const useKeyboard = ({
445
498
  } else if (e.key === 'p' && !e.metaKey && !e.ctrlKey) {
446
499
  if (isVersionPreviewMode) return
447
500
  e.preventDefault()
448
- useEditor.getState().setPhase('structure')
449
- useEditor.getState().setStructureLayer('elements')
450
- useEditor.getState().armMaterialPaint()
501
+ armPaintFromShortcut()
451
502
  } else if (e.key === 'g' && !e.metaKey && !e.ctrlKey) {
452
503
  if (isVersionPreviewMode) return
453
504
  e.preventDefault()
@@ -679,34 +730,14 @@ export const useKeyboard = ({
679
730
  }
680
731
  }
681
732
  } else if ((e.key === 'e' || e.key === 'E') && !isVersionPreviewMode) {
682
- // Toggle door / operable-window open/closed state. Moved off R,
683
- // which now flips the opening (side + π rotation).
733
+ // E runs the kind's interaction or mechanism (doors and windows open and
734
+ // close). Moved off R, which now flips the opening (side + π rotation).
684
735
  const selectedNodeIds = useViewer.getState().selection.selectedIds as AnyNodeId[]
685
736
  if (selectedNodeIds.length === 1) {
686
737
  const node = useScene.getState().nodes[selectedNodeIds[0]!]
687
- const registryE = node && nodeRegistry.get(node.type)?.keyboardActions?.e
688
- if (node && registryE?.appliesTo(node)) {
738
+ if (node && runNodeInteraction(node)) {
689
739
  // Registry-driven E interaction. Same shape as the R/T arms.
690
740
  e.preventDefault()
691
- registryE.run(node)
692
- sfxEmitter.emit('sfx:item-rotate')
693
- } else if (node?.type === 'door' && node.openingKind !== 'opening') {
694
- e.preventDefault()
695
- toggleDoorOpenState(node.id)
696
- sfxEmitter.emit('sfx:item-rotate')
697
- } else if (
698
- node?.type === 'window' &&
699
- node.openingKind !== 'opening' &&
700
- (node.windowType === 'sliding' ||
701
- node.windowType === 'casement' ||
702
- node.windowType === 'awning' ||
703
- node.windowType === 'hopper' ||
704
- node.windowType === 'single-hung' ||
705
- node.windowType === 'double-hung' ||
706
- node.windowType === 'louvered')
707
- ) {
708
- e.preventDefault()
709
- toggleWindowOpenState(node.id)
710
741
  sfxEmitter.emit('sfx:item-rotate')
711
742
  }
712
743
  }