@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
@@ -6,6 +6,8 @@ import {
6
6
  type AnyNodeId,
7
7
  bboxAnchors,
8
8
  bboxCornerAnchors,
9
+ beginSceneHistoryDraft,
10
+ beginSceneHistoryPauseSession,
9
11
  cascadeDirty,
10
12
  collectDescendants,
11
13
  createSceneApi,
@@ -15,8 +17,10 @@ import {
15
17
  getEffectiveNode,
16
18
  type MovableConfig,
17
19
  nodeRegistry,
18
- pauseSceneHistory,
19
- resumeSceneHistory,
20
+ runAsSingleSceneHistoryStep,
21
+ runSceneHistoryDraftWrite,
22
+ sceneHistoryDraftRevertUpdates,
23
+ settleSceneHistoryDrafts,
20
24
  useLiveNodeOverrides,
21
25
  useLiveTransforms,
22
26
  useScene,
@@ -35,6 +39,7 @@ import {
35
39
  import { movementSfxStepKey } from '../../lib/sfx/movement-tick'
36
40
  import { sfxEmitter } from '../../lib/sfx-bus'
37
41
  import { surfaceAttachmentId } from '../../lib/surface-attachment'
42
+ import { isTypingTarget } from '../../lib/typing-target'
38
43
  import { resolveAlignmentForFloorplanView } from '../../lib/world-grid-snap'
39
44
  import useAlignmentGuides from '../../store/use-alignment-guides'
40
45
  import useEditor, {
@@ -146,15 +151,55 @@ export function FloorplanRegistryMoveOverlay() {
146
151
  }) => FloorplanMoveTargetSession
147
152
  )({ node: movingNode, nodes: sceneNodes, sceneApi })
148
153
 
149
- // Capture snapshots of every affected node BEFORE the first apply
150
- // so the single-undo dance has a clean baseline to revert to.
151
- const snapshots = session.affectedIds
152
- .map((id) => sceneNodes[id])
153
- .filter((n): n is AnyNode => !!n)
154
- .map((n) => snapshotNode(n))
154
+ const initialAffectedIds = session.affectedIds.filter((id) => sceneNodes[id])
155
155
 
156
- pauseSceneHistory(useScene)
156
+ // History is paused only around this overlay's own writes (`ownWrite`): every affected
157
+ // node is a carried draft (core's history-drafts), so writes others make mid-move record
158
+ // as their own steps and space detection reconciles them. The drop writes through a
159
+ // session keyed by the moving node, which the 3D mover shares in split view.
160
+ const draftEnds = initialAffectedIds.map((id) =>
161
+ beginSceneHistoryDraft(
162
+ id,
163
+ isFreshPlacementMetadata(sceneNodes[id]!.metadata) ? null : sceneNodes[id]!,
164
+ ),
165
+ )
157
166
  let historyPaused = true
167
+ const endDrafts = () => {
168
+ for (const end of draftEnds) end()
169
+ historyPaused = false
170
+ }
171
+ const ownWrite = runSceneHistoryDraftWrite
172
+ // Puts back only the fields this move wrote and still holds; a rename or any other write
173
+ // someone made mid-move stays.
174
+ // The drop ends the carry: a transient marker (the 3D mover's, in split view) goes even
175
+ // when someone else edited the metadata meanwhile and the move no longer holds it.
176
+ const endTransientMarkers = () => {
177
+ const nodes = useScene.getState().nodes
178
+ const updates = session.affectedIds.flatMap((id) => {
179
+ const metadata = nodes[id]?.metadata as Record<string, unknown> | undefined
180
+ if (!metadata?.isTransient) return []
181
+ const { isTransient: _transient, ...rest } = metadata
182
+ return [{ id, data: { metadata: rest } }]
183
+ })
184
+ if (updates.length > 0) useScene.getState().updateNodes(updates)
185
+ }
186
+ const revertOwnWrites = () =>
187
+ ownWrite(() => {
188
+ const updates = sceneHistoryDraftRevertUpdates(session.affectedIds)
189
+ if (updates.length > 0) useScene.getState().updateNodes(updates)
190
+ })
191
+ const recordDrop = (write: () => void) => {
192
+ const drop = beginSceneHistoryPauseSession(useScene, { gesture: movingNode.id })
193
+ try {
194
+ // The move's write and its transient cleanup are one undo entry and one commit.
195
+ drop.commitStep(() => runAsSingleSceneHistoryStep(useScene, write))
196
+ // Only a successful drop ends ownership; a rejected write can still be retried.
197
+ settleSceneHistoryDrafts(session.affectedIds)
198
+ endDrafts()
199
+ } finally {
200
+ drop.end()
201
+ }
202
+ }
158
203
 
159
204
  const clearLivePreviews = () => {
160
205
  const liveTransforms = useLiveTransforms.getState()
@@ -229,15 +274,17 @@ export function FloorplanRegistryMoveOverlay() {
229
274
  hasMovedSinceStart = true
230
275
  lastPlanPoint = planPoint
231
276
  clearRejection()
232
- session.apply({
233
- planPoint,
234
- modifiers: {
235
- shiftKey: event.shiftKey,
236
- altKey: event.altKey,
237
- ctrlKey: event.ctrlKey,
238
- metaKey: event.metaKey,
239
- },
240
- })
277
+ ownWrite(() =>
278
+ session.apply({
279
+ planPoint,
280
+ modifiers: {
281
+ shiftKey: event.shiftKey,
282
+ altKey: event.altKey,
283
+ ctrlKey: event.ctrlKey,
284
+ metaKey: event.metaKey,
285
+ },
286
+ }),
287
+ )
241
288
  // Move "tick" — same feedback the 3D move gives, which fires whenever the
242
289
  // resolved position changes (any snapping mode, not just grid), so it
243
290
  // ticks as the item lands on each new snapped/free position.
@@ -265,7 +312,14 @@ export function FloorplanRegistryMoveOverlay() {
265
312
  }
266
313
 
267
314
  const commitFinalStateOrRevert = () => {
268
- const commitValid = session.canCommit()
315
+ if (!useScene.getState().nodes[movingNode.id]) {
316
+ revertOwnWrites()
317
+ endDrafts()
318
+ clearLivePreviews()
319
+ return
320
+ }
321
+ // Some registered sessions stage their final polygon from canCommit.
322
+ const commitValid = ownWrite(() => session.canCommit())
269
323
  const freshPlacement = isFreshPlacementMetadata(
270
324
  (useScene.getState().nodes[movingNode.id] as { metadata?: unknown } | undefined)
271
325
  ?.metadata,
@@ -283,7 +337,7 @@ export function FloorplanRegistryMoveOverlay() {
283
337
 
284
338
  // Keep ordinary teardown order. Validated replacements can refuse, so they
285
339
  // claim teardown only after success, before clearing the moving node.
286
- if (!atomicPreview) setMovingNodeOrigin('2d')
340
+ if (!freshPlacement) setMovingNodeOrigin('2d')
287
341
 
288
342
  // Sessions with a `commit` hook own their atomic write (e.g.
289
343
  // wall move emits creates + deletes + updates via the junction
@@ -293,7 +347,7 @@ export function FloorplanRegistryMoveOverlay() {
293
347
  if (commitValid && freshPlacement) {
294
348
  // Subtrees finalize from the preview. Staging a session commit would publish
295
349
  // an unvalidated graph, including for presets created outside our factory.
296
- if (!atomicPreview) session.commit?.()
350
+ if (!atomicPreview) ownWrite(() => session.commit?.())
297
351
  const stagedNode = useScene.getState().nodes[movingNode.id]
298
352
  const preview = usePlacementPreview.getState().node
299
353
  const effective = stagedNode ? getEffectiveNode(stagedNode) : null
@@ -310,67 +364,66 @@ export function FloorplanRegistryMoveOverlay() {
310
364
  showRejection,
311
365
  )
312
366
  : null
313
- if (!committedId && atomicPreview) return false
367
+ if (!committedId) return false
368
+ setMovingNodeOrigin('2d')
314
369
  if (atomicPreview) {
315
- setMovingNodeOrigin('2d')
316
370
  for (const id of session.affectedIds) {
317
371
  useLiveTransforms.getState().clear(id)
318
372
  useLiveNodeOverrides.getState().clear(id)
319
373
  }
320
374
  }
321
- if (historyPaused) {
322
- resumeSceneHistory(useScene)
323
- historyPaused = false
324
- }
325
375
  if (committedId) {
376
+ settleSceneHistoryDrafts(session.affectedIds)
326
377
  sfxEmitter.emit('sfx:item-place')
327
378
  useViewer.getState().setSelection({ selectedIds: [committedId] })
328
379
  }
380
+ endDrafts()
329
381
  return
330
382
  }
331
383
 
332
384
  if (commitValid && session.commit) {
333
- restoreSnapshots(snapshots)
334
- if (historyPaused) {
335
- resumeSceneHistory(useScene)
336
- historyPaused = false
337
- }
338
- session.commit()
385
+ revertOwnWrites()
386
+ recordDrop(() => {
387
+ session.commit?.()
388
+ endTransientMarkers()
389
+ })
339
390
  sfxEmitter.emit('sfx:item-place')
340
- useViewer.getState().setSelection({ selectedIds: snapshots.map((s) => s.id) })
391
+ useViewer.getState().setSelection({ selectedIds: initialAffectedIds })
341
392
  return
342
393
  }
343
394
 
344
395
  const sceneState = useScene.getState().nodes
345
396
  const finalUpdates: Array<{ id: AnyNodeId; data: Record<string, unknown> }> = []
346
- for (const snap of snapshots) {
347
- const current = sceneState[snap.id]
397
+ // Reapply everything the revert removes, including newly introduced fields and
398
+ // attachment entries held on hosts outside affectedIds. Foreign writes stay recorded.
399
+ for (const update of sceneHistoryDraftRevertUpdates(session.affectedIds)) {
400
+ const current = sceneState[update.id]
348
401
  if (!current) continue
349
402
  const data: Record<string, unknown> = {}
350
403
  let changed = false
351
- for (const [key, before] of Object.entries(snap.data)) {
404
+ for (const [key, before] of Object.entries(update.data)) {
352
405
  const after = (current as unknown as Record<string, unknown>)[key]
353
406
  if (!deepEqual(before, after)) {
354
407
  data[key] = Array.isArray(after) ? [...(after as unknown[])] : after
355
408
  changed = true
356
409
  }
357
410
  }
358
- if (changed) finalUpdates.push({ id: snap.id, data })
411
+ if (changed) finalUpdates.push({ id: update.id, data })
359
412
  }
360
413
 
361
- for (const snap of snapshots) {
362
- const current = sceneState[snap.id]
414
+ for (const id of initialAffectedIds) {
415
+ const current = sceneState[id]
363
416
  if (!current || !isFreshPlacementMetadata((current as { metadata?: unknown }).metadata)) {
364
417
  continue
365
418
  }
366
- const existing = finalUpdates.find((update) => update.id === snap.id)
419
+ const existing = finalUpdates.find((update) => update.id === id)
367
420
  const metadata = stripPlacementMetadataFlags((current as { metadata?: unknown }).metadata)
368
421
  if (existing) {
369
422
  existing.data.metadata = metadata
370
423
  existing.data.visible = true
371
424
  } else {
372
425
  finalUpdates.push({
373
- id: snap.id,
426
+ id,
374
427
  data: { metadata, visible: true },
375
428
  })
376
429
  }
@@ -381,25 +434,21 @@ export function FloorplanRegistryMoveOverlay() {
381
434
  // 1. Revert to baseline while history is still paused.
382
435
  // 2. Resume history.
383
436
  // 3. Re-apply the final state — recorded as one tracked change.
384
- restoreSnapshots(snapshots)
385
- if (historyPaused) {
386
- resumeSceneHistory(useScene)
387
- historyPaused = false
388
- }
389
- useScene.getState().updateNodes(finalUpdates)
437
+ revertOwnWrites()
438
+ recordDrop(() => {
439
+ useScene.getState().updateNodes(finalUpdates)
440
+ endTransientMarkers()
441
+ })
390
442
  sfxEmitter.emit('sfx:item-place')
391
443
  // Re-select the moved node(s) — mirrors the legacy 3D move
392
444
  // tool. The action menu cleared selection on Move click so
393
445
  // selection-gated affordances (slab/ceiling boundary editor,
394
446
  // etc.) would unmount during the drag; restoring it here
395
447
  // brings them back at the new position.
396
- useViewer.getState().setSelection({ selectedIds: snapshots.map((s) => s.id) })
448
+ useViewer.getState().setSelection({ selectedIds: initialAffectedIds })
397
449
  } else {
398
- restoreSnapshots(snapshots)
399
- if (historyPaused) {
400
- resumeSceneHistory(useScene)
401
- historyPaused = false
402
- }
450
+ revertOwnWrites()
451
+ endDrafts()
403
452
  }
404
453
  }
405
454
 
@@ -449,6 +498,7 @@ export function FloorplanRegistryMoveOverlay() {
449
498
  }
450
499
 
451
500
  const onKey = (event: KeyboardEvent) => {
501
+ if (isTypingTarget(event.target)) return
452
502
  // R flips a directional kind's facing mid-placement (door / window:
453
503
  // front ↔ back). The session records the flip and re-runs its last
454
504
  // apply so the 2D symbol updates immediately; kinds without a facing
@@ -468,13 +518,9 @@ export function FloorplanRegistryMoveOverlay() {
468
518
  if (event.metaKey || event.ctrlKey) return
469
519
  if (!(session.flipSide && hasMovedSinceStart && pointerOverFloorplan)) return
470
520
  if (event.repeat) return
471
- const t = event.target as HTMLElement | null
472
- if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) {
473
- return
474
- }
475
521
  event.preventDefault()
476
522
  event.stopImmediatePropagation()
477
- session.flipSide()
523
+ ownWrite(() => session.flipSide?.())
478
524
  sfxEmitter.emit('sfx:item-rotate')
479
525
  return
480
526
  }
@@ -492,22 +538,16 @@ export function FloorplanRegistryMoveOverlay() {
492
538
  if (isFreshPlacementMetadata((movingNode as { metadata?: unknown }).metadata)) {
493
539
  emitter.emit('tool:cancel')
494
540
  if (!ownsSubtree && !isInteractionSubtreeDraft(movingNode.id))
495
- useScene.getState().deleteNode(movingNode.id)
496
- if (historyPaused) {
497
- resumeSceneHistory(useScene)
498
- historyPaused = false
499
- }
541
+ ownWrite(() => useScene.getState().deleteNode(movingNode.id))
542
+ endDrafts()
500
543
  clearLivePreviews()
501
544
  useAlignmentGuides.getState().clear()
502
545
  setMovingNode(null)
503
546
  return
504
547
  }
505
- // Revert untracked, then resume — no history entry.
506
- restoreSnapshots(snapshots)
507
- if (historyPaused) {
508
- resumeSceneHistory(useScene)
509
- historyPaused = false
510
- }
548
+ // Revert as the overlay's own write: no history entry.
549
+ revertOwnWrites()
550
+ endDrafts()
511
551
  // Clear any live previews the session wrote. Slab / ceiling
512
552
  // 2D move stages a translation delta in `useLiveTransforms`;
513
553
  // wall move publishes `{ start, end, ... }` to
@@ -516,7 +556,7 @@ export function FloorplanRegistryMoveOverlay() {
516
556
  // position.
517
557
  clearLivePreviews()
518
558
  // Restore selection cleared by the action menu's Move click.
519
- useViewer.getState().setSelection({ selectedIds: snapshots.map((s) => s.id) })
559
+ useViewer.getState().setSelection({ selectedIds: initialAffectedIds })
520
560
  setMovingNode(null)
521
561
  }
522
562
 
@@ -535,7 +575,7 @@ export function FloorplanRegistryMoveOverlay() {
535
575
  window.removeEventListener('keydown', onKey, true)
536
576
  // Unmount cleanup. `historyPaused === true` here means none of
537
577
  // our terminal paths (commit, Esc) ran in this overlay — they
538
- // each call `resumeSceneHistory` and flip the flag.
578
+ // each end the drafts and flip the flag.
539
579
  //
540
580
  // If `movingNodeOrigin === '3d'`, a 3D move tool finalised
541
581
  // while our overlay was still mounted (split view); the live
@@ -554,10 +594,10 @@ export function FloorplanRegistryMoveOverlay() {
554
594
  const finalisedBy3D = useEditor.getState().movingNodeOrigin === '3d'
555
595
  if (!finalisedBy3D && !freshPlacement) {
556
596
  if (hasMovedSinceStart) {
557
- restoreSnapshots(snapshots)
597
+ revertOwnWrites()
558
598
  }
559
599
  }
560
- resumeSceneHistory(useScene)
600
+ endDrafts()
561
601
  }
562
602
  // Belt-and-suspenders: clear any live previews on abnormal
563
603
  // unmount paths too. Slab / ceiling sessions write to
@@ -922,6 +962,7 @@ export function FloorplanRegistryMoveOverlay() {
922
962
  }
923
963
 
924
964
  const onKey = (event: KeyboardEvent) => {
965
+ if (isTypingTarget(event.target)) return
925
966
  if (event.key !== 'Escape' && !isHistoryShortcut(event)) return
926
967
  if (isHistoryShortcut(event)) {
927
968
  // ⌘Z mid-move cancels like Escape — keep it from reaching the
@@ -933,11 +974,7 @@ export function FloorplanRegistryMoveOverlay() {
933
974
  if (isFreshPlacement) {
934
975
  emitter.emit('tool:cancel')
935
976
  if (!ownsSubtree && !isInteractionSubtreeDraft(movingNode.id)) {
936
- const temporal = useScene.temporal.getState()
937
- const wasTracking = (temporal as { isTracking?: boolean }).isTracking !== false
938
- if (wasTracking) temporal.pause()
939
- useScene.getState().deleteNode(movingNode.id)
940
- if (wasTracking) temporal.resume()
977
+ runSceneHistoryDraftWrite(() => useScene.getState().deleteNode(movingNode.id))
941
978
  }
942
979
  }
943
980
  for (const relatedEntry of relatedEntries) {
@@ -970,33 +1007,6 @@ export function FloorplanRegistryMoveOverlay() {
970
1007
  return null
971
1008
  }
972
1009
 
973
- // ── Snapshot helpers (shared shape with floorplan-registry-layer) ───
974
- //
975
- // Kept inline here to avoid a circular dependency through a shared
976
- // utility module. If a third call site shows up, extract.
977
-
978
- type NodeSnapshot = { id: AnyNodeId; data: Record<string, unknown> }
979
-
980
- function snapshotNode(node: AnyNode): NodeSnapshot {
981
- const data: Record<string, unknown> = {}
982
- for (const [key, value] of Object.entries(node)) {
983
- if (key === 'id' || key === 'type' || key === 'object') continue
984
- data[key] = Array.isArray(value) ? [...(value as unknown[])] : value
985
- }
986
- return { id: node.id, data }
987
- }
988
-
989
- function restoreSnapshots(snapshots: NodeSnapshot[]) {
990
- const updates = snapshots
991
- .filter(
992
- (s) =>
993
- !useScene.getState().nodes[s.id] ||
994
- !deepEqual(s.data, snapshotNode(useScene.getState().nodes[s.id]!).data),
995
- )
996
- .map((s) => ({ id: s.id, data: s.data }))
997
- if (updates.length) useScene.getState().updateNodes(updates)
998
- }
999
-
1000
1010
  function deepEqual(a: unknown, b: unknown): boolean {
1001
1011
  if (a === b) return true
1002
1012
  if (Array.isArray(a) && Array.isArray(b)) {
@@ -0,0 +1,156 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import type { AnyNode, AnyNodeId } from '@pascal-app/core'
3
+ import { useScene } from '@pascal-app/core'
4
+ import { commitDimensionValue, isDimensionEditAllowed } from './floorplan-dimension-edit-overlay'
5
+
6
+ type Change = { id: AnyNodeId; data: Partial<AnyNode> }
7
+
8
+ function wall(id: string, start: [number, number], end: [number, number]): AnyNode {
9
+ return {
10
+ object: 'node',
11
+ id,
12
+ type: 'wall',
13
+ parentId: 'level_1',
14
+ visible: true,
15
+ metadata: {},
16
+ start,
17
+ end,
18
+ thickness: 0.15,
19
+ children: [],
20
+ } as unknown as AnyNode
21
+ }
22
+
23
+ function nodes(): Record<string, AnyNode> {
24
+ return {
25
+ level_1: {
26
+ object: 'node',
27
+ id: 'level_1',
28
+ type: 'level',
29
+ parentId: null,
30
+ visible: true,
31
+ metadata: {},
32
+ level: 0,
33
+ children: ['wall_a', 'wall_b', 'dim_1'],
34
+ } as unknown as AnyNode,
35
+ wall_a: wall('wall_a', [0, 0], [3, 0]),
36
+ wall_b: wall('wall_b', [3, 0], [3, 4]),
37
+ dim_1: {
38
+ object: 'node',
39
+ id: 'dim_1',
40
+ type: 'construction-dimension',
41
+ parentId: 'level_1',
42
+ visible: true,
43
+ metadata: {},
44
+ children: [],
45
+ } as unknown as AnyNode,
46
+ }
47
+ }
48
+
49
+ let initial: ReturnType<typeof useScene.getState>
50
+ let applied: Change[]
51
+ let updated: Change[]
52
+ let dirty: AnyNodeId[]
53
+
54
+ beforeEach(() => {
55
+ initial = useScene.getState()
56
+ applied = []
57
+ updated = []
58
+ dirty = []
59
+ useScene.setState({
60
+ nodes: nodes() as ReturnType<typeof useScene.getState>['nodes'],
61
+ applyNodeChanges: ((changes: { update?: Change[] }) => {
62
+ applied.push(...(changes.update ?? []))
63
+ }) as ReturnType<typeof useScene.getState>['applyNodeChanges'],
64
+ updateNode: ((id: AnyNodeId, data: Partial<AnyNode>) => {
65
+ updated.push({ id, data })
66
+ }) as ReturnType<typeof useScene.getState>['updateNode'],
67
+ markDirty: ((id: AnyNodeId) => {
68
+ dirty.push(id)
69
+ }) as ReturnType<typeof useScene.getState>['markDirty'],
70
+ })
71
+ })
72
+
73
+ afterEach(() => {
74
+ useScene.setState(initial, true)
75
+ })
76
+
77
+ describe('commitDimensionValue', () => {
78
+ test('a wall dimension drives the wall end and the wall sharing that corner', () => {
79
+ const outcome = commitDimensionValue({
80
+ target: {
81
+ ownerNodeId: 'wall_a' as AnyNodeId,
82
+ witnessStart: [0, 0],
83
+ witnessEnd: [3, 0],
84
+ value: 3,
85
+ },
86
+ nextLength: 3.5,
87
+ })
88
+
89
+ expect(outcome).toEqual({ ok: true })
90
+ const byId = new Map(applied.map((change) => [change.id, change.data]))
91
+ expect(byId.get('wall_a' as AnyNodeId)).toEqual({ start: [0, 0], end: [3.5, 0] })
92
+ expect(byId.get('wall_b' as AnyNodeId)).toEqual({ start: [3.5, 0], end: [3, 4] })
93
+ expect(dirty.sort()).toEqual(['wall_a', 'wall_b'] as AnyNodeId[])
94
+ expect(updated).toEqual([])
95
+ })
96
+
97
+ test('an undrivable wall dimension is refused and nothing is written', () => {
98
+ const outcome = commitDimensionValue({
99
+ target: {
100
+ ownerNodeId: 'wall_a' as AnyNodeId,
101
+ witnessStart: [1.5, -1],
102
+ witnessEnd: [1.5, 1],
103
+ value: 2,
104
+ },
105
+ nextLength: 2.5,
106
+ })
107
+
108
+ expect(outcome.ok).toBe(false)
109
+ expect(applied).toEqual([])
110
+ expect(updated).toEqual([])
111
+ })
112
+
113
+ test('an undrivable construction dimension keeps the typed text as an override', () => {
114
+ const outcome = commitDimensionValue({
115
+ target: {
116
+ ownerNodeId: 'dim_1' as AnyNodeId,
117
+ witnessStart: [10, 10],
118
+ witnessEnd: [12, 10],
119
+ value: 2,
120
+ },
121
+ nextLength: 3,
122
+ unit: 'metric',
123
+ })
124
+
125
+ expect(outcome).toEqual({ ok: true })
126
+ expect(applied).toEqual([])
127
+ expect(updated).toHaveLength(1)
128
+ expect(updated[0]?.id).toBe('dim_1' as AnyNodeId)
129
+ expect(typeof (updated[0]!.data as { textOverride?: unknown }).textOverride).toBe('string')
130
+ })
131
+ })
132
+
133
+ describe('isDimensionEditAllowed', () => {
134
+ const idle = {
135
+ workspaceMode: 'edit',
136
+ mode: 'select',
137
+ isPreviewMode: false,
138
+ isCaptureMode: false,
139
+ isFirstPersonMode: false,
140
+ } as const
141
+
142
+ test('only an editable scene in select mode of the edit workspace', () => {
143
+ expect(isDimensionEditAllowed(idle, { readOnly: false })).toBe(true)
144
+ expect(isDimensionEditAllowed(idle, { readOnly: true })).toBe(false)
145
+ expect(isDimensionEditAllowed({ ...idle, workspaceMode: 'studio' }, { readOnly: false })).toBe(
146
+ false,
147
+ )
148
+ expect(isDimensionEditAllowed({ ...idle, workspaceMode: 'sheets' }, { readOnly: false })).toBe(
149
+ false,
150
+ )
151
+ expect(isDimensionEditAllowed({ ...idle, mode: 'delete' }, { readOnly: false })).toBe(false)
152
+ expect(isDimensionEditAllowed({ ...idle, isPreviewMode: true }, { readOnly: false })).toBe(
153
+ false,
154
+ )
155
+ })
156
+ })