@pascal-app/editor 1.0.3 → 1.0.5

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 (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -1,4 +1,12 @@
1
- import { expect, test } from 'bun:test'
1
+ import { expect, jest, test } from 'bun:test'
2
+ import type {
3
+ AnyNode,
4
+ AnyNodeDefinition,
5
+ AnyNodeId,
6
+ BuildingNode,
7
+ LevelNode,
8
+ } from '@pascal-app/core'
9
+ import type { Catalogue } from '@react-three/fiber'
2
10
  import { resolveMoveRotationStep } from './move-registry-node-tool'
3
11
 
4
12
  test('applies a free rotation step while the move is unattached', () => {
@@ -8,3 +16,262 @@ test('applies a free rotation step while the move is unattached', () => {
8
16
  test('rejects rotation steps while the move is wall-attached', () => {
9
17
  expect(resolveMoveRotationStep(0.5, 0.25, Math.PI / 2)).toBeNull()
10
18
  })
19
+
20
+ // Contract-derived controller regression, not a helper replay: editable keys
21
+ // leave a live move alone; outside R/T and Alt still affect the real controller.
22
+ // R3F reconciles real Three objects with a rendererless context. No drawing,
23
+ // native typing, canvas hit-testing or GPU evidence is claimed by this test.
24
+ test('focus completion: mounted registry mover owns only outside R T and Alt', async () => {
25
+ const core = await import('@pascal-app/core')
26
+ const { useViewer } = await import('@pascal-app/viewer')
27
+ const { createRoot, extend } = await import('@react-three/fiber')
28
+ const { act, createElement } = await import('react')
29
+ const three = await import('three')
30
+ const { z } = await import('zod')
31
+ const { MoveRegistryNodeTool } = await import('./move-registry-node-tool')
32
+ const { default: useEditor } = await import('../../../store/use-editor')
33
+ const { default: useInteractionScope } = await import('../../../store/use-interaction-scope')
34
+ const { default: useAlignmentGuides } = await import('../../../store/use-alignment-guides')
35
+ const { default: useFacingPose } = await import('../../../store/use-facing-pose')
36
+ const { default: usePlacementPreview } = await import('../../../store/use-placement-preview')
37
+ const globals = [
38
+ 'window',
39
+ 'document',
40
+ 'requestAnimationFrame',
41
+ 'cancelAnimationFrame',
42
+ 'IS_REACT_ACT_ENVIRONMENT',
43
+ ] as const
44
+ const descriptors = globals.map((key) => Object.getOwnPropertyDescriptor(globalThis, key))
45
+ const stores = [
46
+ core.useScene,
47
+ core.useScene.temporal,
48
+ useViewer,
49
+ useEditor,
50
+ useInteractionScope,
51
+ core.useLiveTransforms,
52
+ core.useLiveNodeOverrides,
53
+ useAlignmentGuides,
54
+ useFacingPose,
55
+ usePlacementPreview,
56
+ ]
57
+ const originalStates = stores.map((store) => store.getState())
58
+ const restoreRegistry = core.nodeRegistry._snapshot()
59
+ const sceneObjects = new Map(core.sceneRegistry.nodes)
60
+ const frames = new Map<number, FrameRequestCallback>()
61
+ let nextFrame = 0
62
+ let time = 0
63
+ const flushFrames = () => {
64
+ const pending = [...frames.values()]
65
+ frames.clear()
66
+ for (const callback of pending) {
67
+ time += 16
68
+ callback(time)
69
+ }
70
+ }
71
+ let root: ReturnType<typeof createRoot> | undefined
72
+ let mountedScene: import('three').Scene | undefined
73
+ const mesh = new three.Mesh(new three.BoxGeometry(1, 1, 1), new three.MeshBasicMaterial())
74
+ const id = 'completion_registry' as AnyNodeId
75
+ const buildingId = 'building_completion_registry' as BuildingNode['id']
76
+ const levelId = 'level_completion_registry' as LevelNode['id']
77
+ const node = {
78
+ id,
79
+ type: 'completion:registry',
80
+ object: 'node',
81
+ parentId: levelId,
82
+ children: [],
83
+ metadata: {},
84
+ visible: true,
85
+ position: [2, 0, 3],
86
+ rotation: 0.25,
87
+ } as unknown as AnyNode
88
+ jest.useFakeTimers()
89
+ try {
90
+ const target = new EventTarget()
91
+ const documentFixture = { body: { style: { cursor: '' } }, querySelector: () => null }
92
+ Object.defineProperties(globalThis, {
93
+ window: {
94
+ configurable: true,
95
+ value: Object.assign(target, {
96
+ matchMedia: () => Object.assign(new EventTarget(), { matches: false }),
97
+ }),
98
+ },
99
+ document: { configurable: true, value: documentFixture },
100
+ requestAnimationFrame: {
101
+ configurable: true,
102
+ value: (callback: FrameRequestCallback) => {
103
+ frames.set(++nextFrame, callback)
104
+ return nextFrame
105
+ },
106
+ },
107
+ cancelAnimationFrame: { configurable: true, value: (key: number) => frames.delete(key) },
108
+ IS_REACT_ACT_ENVIRONMENT: { configurable: true, value: true },
109
+ })
110
+ core.registerNode({
111
+ kind: node.type,
112
+ schemaVersion: 1,
113
+ category: 'utility',
114
+ schema: z.object({ type: z.literal(node.type) }).passthrough(),
115
+ defaults: () => ({}),
116
+ capabilities: {
117
+ movable: { isValidPosition: () => false },
118
+ floorPlaced: {
119
+ collides: true,
120
+ footprint: (candidate: AnyNode) => ({
121
+ dimensions: [1, 1, 1],
122
+ rotation: [0, (candidate as unknown as { rotation: number }).rotation, 0],
123
+ }),
124
+ },
125
+ },
126
+ } as unknown as AnyNodeDefinition)
127
+ core.useScene.temporal.getState().pause()
128
+ // Placement validity is level-scoped. Without a real selected level the
129
+ // mover deliberately treats placement as valid before consulting the kind.
130
+ core.useScene.setState({
131
+ nodes: {
132
+ [buildingId]: {
133
+ id: buildingId,
134
+ type: 'building',
135
+ parentId: null,
136
+ children: [levelId],
137
+ position: [0, 0, 0],
138
+ rotation: 0,
139
+ } as unknown as AnyNode,
140
+ [levelId]: {
141
+ id: levelId,
142
+ type: 'level',
143
+ parentId: buildingId,
144
+ children: [id],
145
+ elevation: 0,
146
+ height: 3,
147
+ } as unknown as AnyNode,
148
+ [id]: node,
149
+ },
150
+ rootNodeIds: [buildingId],
151
+ })
152
+ core.useScene.temporal.getState().resume()
153
+ core.clearSceneHistory()
154
+ core.useScene.getState().updateNode(id, { name: 'history sentinel' } as never)
155
+ flushFrames()
156
+ useViewer.getState().setSelection({ selectedIds: [id], levelId, buildingId, zoneId: null })
157
+ useEditor.setState({ mode: 'select', tool: null })
158
+ useEditor.getState().setMovingNode(core.useScene.getState().nodes[id]!)
159
+ mesh.position.set(2, 0, 3)
160
+ mesh.rotation.y = 0.25
161
+ core.sceneRegistry.nodes.set(id, mesh)
162
+ extend(three as unknown as Catalogue)
163
+ // Supplying a renderer-shaped context prevents WebGLRenderer construction.
164
+ // Drawing is a hard failure, never a stub returning fake pixels.
165
+ const gl = {
166
+ render: () => {
167
+ throw new Error('CPU controller acceptance must not render')
168
+ },
169
+ setSize: () => {},
170
+ setPixelRatio: () => {},
171
+ domElement: {},
172
+ }
173
+ root = createRoot({} as HTMLCanvasElement)
174
+ await root.configure({
175
+ gl: gl as never,
176
+ frameloop: 'never',
177
+ size: { width: 300, height: 300, top: 0, left: 0 },
178
+ onCreated: (state) => {
179
+ mountedScene = state.scene
180
+ },
181
+ })
182
+ await act(async () => {
183
+ root!.render(
184
+ createElement(MoveRegistryNodeTool, { node: core.useScene.getState().nodes[id]! }),
185
+ )
186
+ })
187
+ const colours = () => {
188
+ const result: number[] = []
189
+ mountedScene!.traverse((object) => {
190
+ const material = (object as import('three').Mesh).material
191
+ for (const entry of Array.isArray(material) ? material : [material]) {
192
+ if (entry && 'color' in entry)
193
+ result.push((entry.color as import('three').Color).getHex())
194
+ }
195
+ })
196
+ return result.sort()
197
+ }
198
+ const snapshot = () =>
199
+ JSON.parse(
200
+ JSON.stringify({
201
+ nodes: core.useScene.getState().nodes,
202
+ live: [...core.useLiveTransforms.getState().transforms],
203
+ overrides: [...core.useLiveNodeOverrides.getState().overrides],
204
+ scope: useInteractionScope.getState().scope,
205
+ history: {
206
+ past: core.useScene.temporal.getState().pastStates,
207
+ future: core.useScene.temporal.getState().futureStates,
208
+ tracking: core.useScene.temporal.getState().isTracking,
209
+ pauseDepth: core.getSceneHistoryPauseDepth(),
210
+ },
211
+ pose: { position: mesh.position.toArray(), rotation: mesh.rotation.y },
212
+ colours: colours(),
213
+ }),
214
+ )
215
+ const key = async (value: string, editable: boolean, type = 'keydown') => {
216
+ const event = new Event(type, { cancelable: true })
217
+ Object.defineProperties(event, {
218
+ key: { value },
219
+ target: {
220
+ value: editable
221
+ ? { tagName: 'TEXTAREA', closest: () => ({ tagName: 'TEXTAREA' }) }
222
+ : target,
223
+ },
224
+ altKey: { value: value === 'Alt' },
225
+ ctrlKey: { value: false },
226
+ metaKey: { value: false },
227
+ })
228
+ await act(async () => {
229
+ target.dispatchEvent(event)
230
+ flushFrames()
231
+ })
232
+ return event
233
+ }
234
+ expect((await key('r', false)).defaultPrevented).toBe(true)
235
+ expect(mesh.rotation.y).toBeCloseTo(0.25 + Math.PI / 4)
236
+ expect(colours()).toContain(0xef4444)
237
+ const before = snapshot()
238
+ expect(before.live.length).toBeGreaterThan(0)
239
+ expect(before.history.past.length).toBeGreaterThan(0)
240
+ expect(before.history.tracking).toBe(false)
241
+ expect(before.scope.kind).toBe('moving')
242
+ for (const value of ['r', 't', 'Alt']) {
243
+ expect((await key(value, true)).defaultPrevented).toBe(false)
244
+ expect(snapshot()).toEqual(before)
245
+ }
246
+ // Key-up reset remains active even if focus moved into a field.
247
+ await key('Alt', false)
248
+ expect(colours()).toContain(0x22c55e)
249
+ await key('Alt', true, 'keyup')
250
+ expect(colours()).toContain(0xef4444)
251
+ expect((await key('t', false)).defaultPrevented).toBe(true)
252
+ expect(mesh.rotation.y).toBeCloseTo(0.25)
253
+ expect(core.useLiveTransforms.getState().transforms.get(id)?.rotation).toBeCloseTo(0.25)
254
+ } finally {
255
+ if (root)
256
+ await act(async () => {
257
+ root!.unmount()
258
+ })
259
+ // R3F's delayed disposal is advanced, not slept through, before globals go.
260
+ jest.runOnlyPendingTimers()
261
+ frames.clear()
262
+ restoreRegistry()
263
+ core.sceneRegistry.nodes.clear()
264
+ for (const [key, value] of sceneObjects) core.sceneRegistry.nodes.set(key, value)
265
+ stores.forEach((store, index) => {
266
+ ;(store.setState as (snapshot: unknown, replace: true) => void)(originalStates[index], true)
267
+ })
268
+ mesh.geometry.dispose()
269
+ mesh.material.dispose()
270
+ jest.useRealTimers()
271
+ globals.forEach((key, index) => {
272
+ const descriptor = descriptors[index]
273
+ if (descriptor) Object.defineProperty(globalThis, key, descriptor)
274
+ else Reflect.deleteProperty(globalThis, key)
275
+ })
276
+ }
277
+ }, 15_000)
@@ -13,9 +13,11 @@ import {
13
13
  createSceneApi,
14
14
  emitter,
15
15
  findLevelAncestorId,
16
+ floorPlacedCollides,
16
17
  footprintAABBFrom,
17
18
  type GridEvent,
18
19
  type GroupMoveSnapResult,
20
+ getFloorPlacedElevation,
19
21
  getFloorPlacedFootprints,
20
22
  type MovableConfig,
21
23
  movingFootprintAnchors,
@@ -59,6 +61,7 @@ import {
59
61
  surfaceAttachmentUpdates,
60
62
  updateSurfaceNode,
61
63
  } from '../../../lib/surface-attachment'
64
+ import { isTypingTarget } from '../../../lib/typing-target'
62
65
  import useAlignmentGuides from '../../../store/use-alignment-guides'
63
66
  import useEditor, {
64
67
  getActiveSnappingMode,
@@ -340,7 +343,8 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
340
343
  // position isn't in the level frame the spatial grid indexes. They may
341
344
  // still provide a parent-frame collision check and use the same bounds box.
342
345
  const collides =
343
- !frameParent && nodeRegistry.get(node.type)?.capabilities?.floorPlaced?.collides === true
346
+ !frameParent &&
347
+ floorPlacedCollides(nodeRegistry.get(node.type)?.capabilities?.floorPlaced, node)
344
348
  // Snapshot the scene once at drag-start — bounds depend on `node` (locked
345
349
  // for the lifetime of this tool) and any sibling state the kind reads. If a
346
350
  // future kind needs live sibling state mid-drag, switch to a subscribed
@@ -414,6 +418,9 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
414
418
  const [cursorRotationY, setCursorRotationY] = useState(() => previewRotationY(originalRotationY))
415
419
  const { isFreshPlacement, previewVisible, revealFreshPlacement, useAbsoluteCursorPlacement } =
416
420
  useFreshPlacementVisibility({ node })
421
+ const preserveLevelAndElevation =
422
+ !isFreshPlacement &&
423
+ nodeRegistry.get(node.type)?.capabilities?.movable?.preserveLevelAndElevation === true
417
424
  // Kinds that declare `movable.cursorAttached` (duct fittings) pin to the
418
425
  // cursor instead of preserving the grab offset — small connector-like
419
426
  // nodes read an offset drag as "lagging behind the mouse".
@@ -477,7 +484,24 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
477
484
  return Array.isArray(rotation) ? [rotation[0] ?? 0, y, rotation[2] ?? 0] : y
478
485
  }
479
486
 
487
+ const keepStartingElevation = (position: [number, number, number], yaw: number) => {
488
+ if (!preserveLevelAndElevation) return position
489
+ const rotation = toCommitRotation(yaw)
490
+ const currentNodes = useScene.getState().nodes
491
+ const effectiveNode = { ...node, position, rotation } as AnyNode
492
+ const lift = getFloorPlacedElevation({
493
+ node: effectiveNode,
494
+ nodes: { ...currentNodes, [node.id]: effectiveNode },
495
+ position,
496
+ rotation,
497
+ })
498
+ return [position[0], originalPlanPosition[1] - lift, position[2]] as [number, number, number]
499
+ }
500
+
480
501
  const currentLevelId = () =>
502
+ (preserveLevelAndElevation
503
+ ? findLevelAncestorId(node.id, useScene.getState().nodes)
504
+ : null) ??
481
505
  useViewer.getState().selection.levelId ??
482
506
  (itemSurfaceMove ? findLevelAncestorId(node.id, useScene.getState().nodes) : node.parentId)
483
507
 
@@ -803,10 +827,14 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
803
827
  // test are plane-height independent, so the elected surface is a
804
828
  // single fixed point per pointer ray.
805
829
  const pointed = resolvePointerSupportSurface(cameraRef.current, event.position)
806
- supportCapRef.current = pointed?.elevation ?? null
807
- supportSurfaceRef.current = pointed
808
- const rawX = pointed?.localPoint?.[0] ?? event.localPosition[0]
809
- const rawZ = pointed?.localPoint?.[2] ?? event.localPosition[2]
830
+ supportCapRef.current = preserveLevelAndElevation ? null : (pointed?.elevation ?? null)
831
+ supportSurfaceRef.current = preserveLevelAndElevation ? null : pointed
832
+ const rawX = preserveLevelAndElevation
833
+ ? event.localPosition[0]
834
+ : (pointed?.localPoint?.[0] ?? event.localPosition[0])
835
+ const rawZ = preserveLevelAndElevation
836
+ ? event.localPosition[2]
837
+ : (pointed?.localPoint?.[2] ?? event.localPosition[2])
810
838
  revealFreshPlacement()
811
839
 
812
840
  const magnetic = isMagneticSnapActive()
@@ -980,7 +1008,7 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
980
1008
  if (guides.length > 0) useAlignmentGuides.getState().set(guides)
981
1009
  }
982
1010
  }
983
- if (!parentFrame && pointed?.sourceNodeId) {
1011
+ if (!preserveLevelAndElevation && !parentFrame && pointed?.sourceNodeId) {
984
1012
  const rotation = toCommitRotation(rotationRef.current)
985
1013
  const effectiveNode = {
986
1014
  ...((useScene.getState().nodes[node.id] ?? node) as Record<string, unknown>),
@@ -998,6 +1026,7 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
998
1026
  },
999
1027
  ).position
1000
1028
  }
1029
+ position = keepStartingElevation(position, rotationRef.current)
1001
1030
  const visualPosition = getVisualPosition(position)
1002
1031
  hasMovedRef.current = true
1003
1032
  setCursorPosition(visualPosition)
@@ -1105,18 +1134,20 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
1105
1134
  // The pointer cap makes the persisted host reproduce the capped
1106
1135
  // election — a drop under a deck stores the aimed-at lower slab
1107
1136
  // (or the ground), not the deck hanging above.
1108
- ...resolveSupportSlabPatch(
1109
- effectiveNode,
1110
- {
1111
- ...useScene.getState().nodes,
1112
- [node.id]: effectiveNode,
1113
- },
1114
- {
1115
- maxElevation: supportCapRef.current,
1116
- preferredSlabId: supportSurfaceRef.current?.supportSlabId,
1117
- pinSupport: supportSurfaceRef.current?.sourceNodeId != null,
1118
- },
1119
- ),
1137
+ ...(preserveLevelAndElevation
1138
+ ? {}
1139
+ : resolveSupportSlabPatch(
1140
+ effectiveNode,
1141
+ {
1142
+ ...useScene.getState().nodes,
1143
+ [node.id]: effectiveNode,
1144
+ },
1145
+ {
1146
+ maxElevation: supportCapRef.current,
1147
+ preferredSlabId: supportSurfaceRef.current?.supportSlabId,
1148
+ pinSupport: supportSurfaceRef.current?.sourceNodeId != null,
1149
+ },
1150
+ )),
1120
1151
  ...(isNew
1121
1152
  ? {
1122
1153
  metadata: stripPlacementMetadataFlags(node.metadata),
@@ -1189,18 +1220,20 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
1189
1220
  const committedNode = def.schema.parse({
1190
1221
  ...reparsed,
1191
1222
  parentId: node.parentId,
1192
- ...resolveSupportSlabPatch(
1193
- { ...reparsed, parentId: node.parentId } as AnyNode,
1194
- {
1195
- ...useScene.getState().nodes,
1196
- [reparsed.id]: reparsed,
1197
- },
1198
- {
1199
- maxElevation: supportCapRef.current,
1200
- preferredSlabId: supportSurfaceRef.current?.supportSlabId,
1201
- pinSupport: supportSurfaceRef.current?.sourceNodeId != null,
1202
- },
1203
- ),
1223
+ ...(preserveLevelAndElevation
1224
+ ? {}
1225
+ : resolveSupportSlabPatch(
1226
+ { ...reparsed, parentId: node.parentId } as AnyNode,
1227
+ {
1228
+ ...useScene.getState().nodes,
1229
+ [reparsed.id]: reparsed,
1230
+ },
1231
+ {
1232
+ maxElevation: supportCapRef.current,
1233
+ preferredSlabId: supportSurfaceRef.current?.supportSlabId,
1234
+ pinSupport: supportSurfaceRef.current?.sourceNodeId != null,
1235
+ },
1236
+ )),
1204
1237
  }) as AnyNode
1205
1238
  useScene.temporal.getState().resume()
1206
1239
  useScene.getState().createNode(committedNode, node.parentId as AnyNodeId)
@@ -1245,6 +1278,7 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
1245
1278
  // item placement keys (and the "Rotate" hints the move HUD shows). Applied
1246
1279
  // imperatively + mirrored to the live transform; committed on drop.
1247
1280
  const onKeyDown = (e: KeyboardEvent) => {
1281
+ if (isTypingTarget(e.target)) return
1248
1282
  // Hold Alt (free place) to force placement on an invalid (red) footprint,
1249
1283
  // matching the GLB item tool. Recolour the box to green while held.
1250
1284
  if (e.key === 'Alt') {
@@ -1285,6 +1319,7 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
1285
1319
  )
1286
1320
  position = canonicalPositionFromPlan(planOrigin[0], position[1], planOrigin[2])
1287
1321
  }
1322
+ position = keepStartingElevation(position, hostedPose?.rotationY ?? nextFreeRotation)
1288
1323
  lastCursorRef.current = position
1289
1324
  freeRotationRef.current = hostedPose?.rotationY ?? nextFreeRotation
1290
1325
  rotationRef.current = freeRotationRef.current
@@ -1421,6 +1456,7 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
1421
1456
  originalPosition,
1422
1457
  originalPlanPosition,
1423
1458
  originalRotationY,
1459
+ preserveLevelAndElevation,
1424
1460
  previewRotationY,
1425
1461
  revealFreshPlacement,
1426
1462
  useAbsoluteCursorPlacement,
@@ -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
+ })