@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
@@ -3,14 +3,24 @@ import {
3
3
  type AnyNode,
4
4
  type AnyNodeDefinition,
5
5
  type AnyNodeId,
6
+ BuildingNode,
7
+ beginSceneHistoryPauseSession,
6
8
  CabinetModuleNode,
7
9
  CabinetNode,
8
10
  findLevelAncestorId,
11
+ getSceneHistoryPauseDepth,
12
+ type LevelNode,
9
13
  nodeRegistry,
14
+ pauseSceneHistory,
15
+ RoofNode,
16
+ RoofSegmentNode,
10
17
  registerNode,
18
+ resumeSceneHistory,
11
19
  type SceneCommit,
12
20
  subscribeSceneCommits,
13
21
  useScene,
22
+ WallNode,
23
+ WindowNode,
14
24
  } from '@pascal-app/core'
15
25
  import { z } from 'zod'
16
26
  import {
@@ -187,6 +197,8 @@ describe('commitFreshPlacementSubtree', () => {
187
197
  } as Partial<AnyNode>)
188
198
  unsubscribe()
189
199
 
200
+ // A legacy caller's raw pause is kept after the one recorded step.
201
+ expect(useScene.temporal.getState().isTracking).toBe(false)
190
202
  expect(committedId).toBeTruthy()
191
203
  expect(committedId).not.toBe(SHELF_ID)
192
204
  const finalId = committedId as AnyNodeId
@@ -217,6 +229,132 @@ describe('commitFreshPlacementSubtree', () => {
217
229
  expect((useScene.getState().nodes[LEVEL_ID] as { children: AnyNodeId[] }).children).toEqual([])
218
230
  })
219
231
 
232
+ // A roof lives on the level above the walls that carry it: one dropped onto them (a duplicate
233
+ // in hand) goes there in the placement's one step.
234
+ test('a roof dropped onto the walls that carry it lands on a new Roof level, in the one step', () => {
235
+ const corners: [number, number][] = [
236
+ [0, 0],
237
+ [8, 0],
238
+ [8, 6],
239
+ [0, 6],
240
+ ]
241
+ const walls = corners.map((start, i) =>
242
+ WallNode.parse({ parentId: LEVEL_ID, start, end: corners[(i + 1) % 4], height: 2.5 }),
243
+ )
244
+ const segment = RoofSegmentNode.parse({ width: 8, depth: 6, wallHeight: 0 })
245
+ const draft = RoofNode.parse({
246
+ parentId: LEVEL_ID,
247
+ visible: false,
248
+ metadata: { isNew: true },
249
+ children: [segment.id],
250
+ })
251
+ const building = BuildingNode.parse({ children: [LEVEL_ID] })
252
+ useScene.setState({
253
+ nodes: {
254
+ [building.id]: building,
255
+ [LEVEL_ID]: {
256
+ ...level([...walls.map((wall) => wall.id), draft.id]),
257
+ parentId: building.id,
258
+ height: 2.5,
259
+ },
260
+ ...Object.fromEntries(walls.map((wall) => [wall.id, wall])),
261
+ [draft.id]: draft,
262
+ [segment.id]: { ...segment, parentId: draft.id },
263
+ },
264
+ rootNodeIds: [building.id],
265
+ collections: {},
266
+ dirtyNodes: new Set(),
267
+ } as never)
268
+ useScene.temporal.getState().clear()
269
+
270
+ const committedId = commitFreshPlacementSubtree(draft.id, {
271
+ position: [4, 2.5, 3],
272
+ visible: true,
273
+ } as Partial<AnyNode>)!
274
+ const nodes = useScene.getState().nodes
275
+ const roofLevel = nodes[nodes[committedId]!.parentId as AnyNodeId] as LevelNode
276
+ expect(roofLevel).toMatchObject({ type: 'level', metadata: { role: 'roof' } })
277
+ expect(roofLevel.children as string[]).toEqual([committedId])
278
+ expect((nodes[LEVEL_ID] as LevelNode).children as string[]).not.toContain(committedId)
279
+ expect(useScene.getState().rootNodeIds).toEqual([building.id])
280
+
281
+ useScene.temporal.getState().undo()
282
+ expect(useScene.getState().nodes[committedId]).toBeUndefined()
283
+ expect(useScene.getState().nodes[roofLevel.id]).toBeUndefined()
284
+ })
285
+
286
+ test('a window joined at a corner while in hand stays joined: its partner names the committed window', () => {
287
+ const wall = WallNode.parse({ id: 'wall_a', parentId: LEVEL_ID, start: [4, 0], end: [0, 0] })
288
+ const other = WallNode.parse({ id: 'wall_b', parentId: LEVEL_ID, start: [0, 0], end: [0, 3] })
289
+ const draft = WindowNode.parse({
290
+ id: 'window_draft',
291
+ parentId: wall.id,
292
+ wallId: wall.id,
293
+ position: [3.5, 1.5, 0],
294
+ width: 1,
295
+ visible: false,
296
+ metadata: { isNew: true },
297
+ corner: { end: 'end', partnerId: 'window_partner', post: 'none' },
298
+ })
299
+ const partner = WindowNode.parse({
300
+ id: 'window_partner',
301
+ parentId: other.id,
302
+ wallId: other.id,
303
+ position: [0.5, 1.5, 0],
304
+ width: 1,
305
+ corner: { end: 'start', partnerId: draft.id, post: 'none' },
306
+ })
307
+ useScene.setState({
308
+ nodes: {
309
+ [LEVEL_ID]: level([wall.id as AnyNodeId, other.id as AnyNodeId]),
310
+ [wall.id]: { ...wall, children: [draft.id] } as AnyNode,
311
+ [other.id]: { ...other, children: [partner.id] } as AnyNode,
312
+ [draft.id]: draft as AnyNode,
313
+ [partner.id]: partner as AnyNode,
314
+ },
315
+ rootNodeIds: [LEVEL_ID],
316
+ collections: {},
317
+ dirtyNodes: new Set(),
318
+ } as never)
319
+
320
+ const committedId = commitFreshPlacementSubtree(draft.id as AnyNodeId, { visible: true })
321
+
322
+ const nodes = useScene.getState().nodes as Record<string, AnyNode>
323
+ expect(committedId).toBeTruthy()
324
+ expect((nodes[committedId!] as WindowNode).corner?.partnerId).toBe(partner.id)
325
+ expect((nodes[partner.id] as WindowNode).corner?.partnerId).toBe(committedId!)
326
+ })
327
+
328
+ test("never records inside another owner's pause", () => {
329
+ pauseSceneHistory(useScene)
330
+ try {
331
+ const committedId = commitFreshPlacementSubtree(SHELF_ID, {
332
+ position: [2, 0, 3],
333
+ visible: true,
334
+ } as Partial<AnyNode>)
335
+ expect(committedId).toBeTruthy()
336
+ expect(useScene.temporal.getState().pastStates).toHaveLength(0)
337
+ expect(useScene.temporal.getState().isTracking).toBe(false)
338
+ expect(getSceneHistoryPauseDepth()).toBe(1)
339
+ } finally {
340
+ resumeSceneHistory(useScene)
341
+ }
342
+ })
343
+
344
+ test("lifts the moving node's gesture session for its one step and keeps it after", () => {
345
+ const overlay = beginSceneHistoryPauseSession(useScene, { gesture: SHELF_ID })
346
+ const committedId = commitFreshPlacementSubtree(SHELF_ID, {
347
+ position: [2, 0, 3],
348
+ visible: true,
349
+ } as Partial<AnyNode>)
350
+ expect(committedId).toBeTruthy()
351
+ expect(useScene.temporal.getState().pastStates).toHaveLength(1)
352
+ expect(useScene.temporal.getState().isTracking).toBe(false)
353
+ overlay.end()
354
+ expect(getSceneHistoryPauseDepth()).toBe(0)
355
+ expect(useScene.temporal.getState().isTracking).toBe(true)
356
+ })
357
+
220
358
  test('uses the subtree contract for childless variants and never aliases root-only children', () => {
221
359
  registerCabinetClonePrepTestKind()
222
360
  const childlessCabinet = {
@@ -244,6 +382,16 @@ describe('commitFreshPlacementSubtree', () => {
244
382
  expect((source as AnyNode & { children: AnyNodeId[] }).children).toEqual(['item_original'])
245
383
  })
246
384
 
385
+ // A copy of one side of a corner window kept naming the original's partner, which never named it
386
+ // back: a link that made the copy refuse to join another window.
387
+ test('a copy of one side of a corner window is a plain window', () => {
388
+ const side = WindowNode.parse({
389
+ corner: { end: 'end', partnerId: 'window_partner', post: 'none' },
390
+ })
391
+ expect((prepareFreshPlacementRootDuplicate(side) as WindowNode).corner).toBeUndefined()
392
+ expect(side.corner?.partnerId).toBe('window_partner')
393
+ })
394
+
247
395
  test('commits a duplicated cabinet draft without deleting the original modules', () => {
248
396
  seedCabinetRun()
249
397
  useScene.temporal.getState().clear()
@@ -1,16 +1,24 @@
1
1
  import {
2
2
  type AnyNode,
3
3
  type AnyNodeId,
4
+ beginSceneHistoryPauseSession,
5
+ type CloneNodesIntoResult,
4
6
  cloneNodesInto,
7
+ collectionIdsOf,
5
8
  collectSubtree,
6
9
  createSceneApi,
7
10
  type DuplicableConfig,
11
+ getEffectiveNode,
12
+ getSceneHistoryPauseDepth,
8
13
  getSurfaceProvider,
9
14
  nodeRegistry,
10
15
  resolveSurfacePlacement,
16
+ runSceneHistoryDraftWrite,
11
17
  type SurfaceRejectReason,
18
+ seatRoofs,
12
19
  useScene,
13
20
  } from '@pascal-app/core'
21
+ import { remapNodeReferences } from '@pascal-app/core/clone-scene-graph'
14
22
  import { evaluateRecipe, isProceduralItem } from '@pascal-app/core/procedural-items'
15
23
  import useInteractionScope from '../store/use-interaction-scope'
16
24
  import usePlacementPreview from '../store/use-placement-preview'
@@ -34,6 +42,61 @@ function parentIdOf(node: AnyNode): AnyNodeId | undefined {
34
42
  return parentId ?? undefined
35
43
  }
36
44
 
45
+ /**
46
+ * The nodes the draft root names, outside its subtree and other than its parent, that name it
47
+ * back (a corner window's partner, joined while the draft was in hand). The commit gives the
48
+ * root a new id, so they are rewritten to name the committed node, never the deleted draft.
49
+ */
50
+ function peerReferenceUpdates(
51
+ nodes: Readonly<Record<string, AnyNode>>,
52
+ root: AnyNode,
53
+ subtreeIds: ReadonlySet<string>,
54
+ committedId: AnyNodeId,
55
+ ): { id: AnyNodeId; data: Partial<AnyNode> }[] {
56
+ const named = new Set<string>()
57
+ const visit = (value: unknown) => {
58
+ if (typeof value === 'string') {
59
+ if (nodes[value] && !subtreeIds.has(value) && value !== root.parentId) named.add(value)
60
+ } else if (Array.isArray(value)) value.forEach(visit)
61
+ else if (value && typeof value === 'object') Object.values(value).forEach(visit)
62
+ }
63
+ const {
64
+ id: _id,
65
+ parentId: _parentId,
66
+ children: _children,
67
+ ...fields
68
+ } = root as AnyNode & {
69
+ children?: unknown
70
+ }
71
+ visit(fields)
72
+ const ids = new Map([[root.id, committedId as string]])
73
+ return [...named].flatMap((id) => {
74
+ const peer = nodes[id]!
75
+ const remapped = remapNodeReferences(peer, ids) as Record<string, unknown>
76
+ const data = Object.fromEntries(
77
+ Object.entries(remapped).filter(
78
+ ([key, value]) =>
79
+ JSON.stringify(value) !== JSON.stringify((peer as Record<string, unknown>)[key]),
80
+ ),
81
+ )
82
+ return Object.keys(data).length ? [{ id: id as AnyNodeId, data: data as Partial<AnyNode> }] : []
83
+ })
84
+ }
85
+
86
+ /**
87
+ * Create ops for a clone of scene nodes: each copy joins the collections its
88
+ * source is in, whatever its kind.
89
+ */
90
+ export function copyCreateOps(cloned: CloneNodesIntoResult, parentId: AnyNodeId | undefined) {
91
+ const collections = useScene.getState().collections
92
+ const sourceIds = new Map([...cloned.idMap].map(([source, copy]) => [copy, source]))
93
+ return cloned.nodes.map((node, index) => ({
94
+ node,
95
+ ...(index === 0 && parentId ? { parentId } : {}),
96
+ collectionIds: collectionIdsOf(collections, sourceIds.get(node.id as AnyNodeId)!),
97
+ }))
98
+ }
99
+
37
100
  function duplicableConfigFor(node: AnyNode): DuplicableConfig | null {
38
101
  const duplicable = nodeRegistry.get(node.type)?.capabilities?.duplicable
39
102
  return duplicable && typeof duplicable === 'object' ? duplicable : null
@@ -61,6 +124,8 @@ export function prepareFreshPlacementRootDuplicate(node: AnyNode): AnyNode {
61
124
  metadata?: unknown
62
125
  }
63
126
  delete duplicate.id
127
+ // A copy of one side of a corner window is a plain window: its partner stays with the original.
128
+ delete duplicate.corner
64
129
  if (Array.isArray(duplicate.children)) duplicate.children = []
65
130
  duplicate.metadata = {
66
131
  ...getPlacementMetadataRecord(stripPlacementMetadataFlags(duplicate.metadata)),
@@ -114,9 +179,7 @@ export function createFreshPlacementSubtree(
114
179
  })
115
180
 
116
181
  scene.applyNodeChanges({
117
- create: cloned.nodes.map((node, index) =>
118
- index === 0 && parentId ? { node, parentId } : { node },
119
- ),
182
+ create: copyCreateOps(cloned, parentId),
120
183
  update: surfaceAttachmentUpdates(cloned.rootId, parentId, surfaceAttachmentId(subtree.root)),
121
184
  })
122
185
 
@@ -202,6 +265,21 @@ function namedSurfaceRejection(
202
265
  return null
203
266
  }
204
267
 
268
+ /**
269
+ * Runs a placement's one committing write as a history step. The moving node's gesture
270
+ * sessions (the 2D move overlay's) are lifted for it; any other owner's pause still holds, so
271
+ * the write then records nothing. A legacy caller's raw `temporal.pause()` is kept afterwards.
272
+ */
273
+ function recordPlacementStep(rootId: AnyNodeId, wasTracking: boolean, write: () => void): void {
274
+ const step = beginSceneHistoryPauseSession(useScene, { gesture: rootId })
275
+ try {
276
+ step.commitStep(write)
277
+ } finally {
278
+ step.end()
279
+ if (!wasTracking && getSceneHistoryPauseDepth() === 0) useScene.temporal.getState().pause()
280
+ }
281
+ }
282
+
205
283
  /**
206
284
  * Replace the draft in one validated write. History already excludes fresh
207
285
  * subtrees, so this records one creation without first deleting the preview.
@@ -223,9 +301,13 @@ export function commitFreshPlacementSubtree(
223
301
  onReject?.(rejection)
224
302
  return null
225
303
  }
226
- const descendants = subtree.descendants.map((node) => cleanPlacementMetadata(node))
304
+ const descendants = subtree.descendants.map((node) =>
305
+ cleanPlacementMetadata(getEffectiveNode(node)),
306
+ )
227
307
  const parentId = parentIdOf(root)
228
308
  const cloned = cloneNodesInto([root, ...descendants], { rootId, parentId })
309
+ // The drafts' memberships, read before they are deleted.
310
+ const create = copyCreateOps(cloned, parentId)
229
311
  const updates = surfaceAttachmentUpdates(rootId, null, null)
230
312
  for (const update of surfaceAttachmentUpdates(cloned.rootId, parentId, surfaceId)) {
231
313
  const attachments = { ...(update.data as { attachments: Record<string, string> }).attachments }
@@ -235,6 +317,12 @@ export function commitFreshPlacementSubtree(
235
317
  if (previous === -1) updates.push(merged)
236
318
  else updates[previous] = merged
237
319
  }
320
+ const subtreeIds = new Set([rootId, ...descendants.map((node) => node.id)])
321
+ for (const update of peerReferenceUpdates(scene.nodes, root, subtreeIds, cloned.rootId))
322
+ if (!updates.some((entry) => entry.id === update.id)) updates.push(update)
323
+
324
+ // A roof dropped onto the walls that carry it goes to the level above them, in the same step.
325
+ const seated = seatRoofs(scene.nodes, { create, update: updates, delete: [rootId] }).changes
238
326
 
239
327
  const scope = useInteractionScope.getState()
240
328
  const factoryCreated =
@@ -243,43 +331,32 @@ export function commitFreshPlacementSubtree(
243
331
  const wasTracking = temporal.isTracking
244
332
  if (!factoryCreated && descendants.length === 0 && surfaceId === null) {
245
333
  // Preserve the established root-only lifecycle for placements outside the subtree factory.
246
- if (wasTracking) temporal.pause()
247
- updateSurfaceNode(rootId, {}, null)
248
- useScene.getState().deleteNode(rootId)
249
- temporal.resume()
250
- useScene.getState().applyNodeChanges({
251
- create: cloned.nodes.map((node, index) =>
252
- index === 0 && parentId ? { node, parentId } : { node },
253
- ),
254
- update: updates,
334
+ runSceneHistoryDraftWrite(() => {
335
+ updateSurfaceNode(rootId, {}, null)
336
+ useScene.getState().deleteNode(rootId)
255
337
  })
256
- if (!wasTracking) temporal.pause()
338
+ recordPlacementStep(rootId, wasTracking, () =>
339
+ useScene.getState().applyNodeChanges({ create: seated.create, update: seated.update }),
340
+ )
257
341
  } else {
258
- temporal.resume()
259
- try {
260
- // applyNodeChanges validates the complete proposed graph before publishing any part of it.
261
- scene.applyNodeChanges({
262
- delete: [rootId],
263
- create: cloned.nodes.map((node, index) =>
264
- index === 0 && parentId ? { node, parentId } : { node },
265
- ),
266
- update: updates,
267
- })
268
- for (const node of [subtree.root, ...subtree.descendants]) scene.clearDirty(node.id)
269
- } catch (error) {
270
- // Zustand publishes before notifying subscribers. A subscriber error must restore
271
- // the draft and its ownership/history, never masquerade as a placement refusal.
272
- temporal.pause()
342
+ recordPlacementStep(rootId, wasTracking, () => {
273
343
  try {
274
- if (useScene.getState() !== scene) useScene.setState(scene, true)
275
- } finally {
276
- useInteractionScope.setState(scope)
277
- useScene.temporal.setState(temporal)
344
+ // applyNodeChanges validates the complete proposed graph before publishing any part of it.
345
+ scene.applyNodeChanges(seated)
346
+ for (const node of [subtree.root, ...subtree.descendants]) scene.clearDirty(node.id)
347
+ } catch (error) {
348
+ // Zustand publishes before notifying subscribers. A subscriber error must restore
349
+ // the draft and its ownership/history, never masquerade as a placement refusal.
350
+ useScene.temporal.getState().pause()
351
+ try {
352
+ if (useScene.getState() !== scene) useScene.setState(scene, true)
353
+ } finally {
354
+ useInteractionScope.setState(scope)
355
+ useScene.temporal.setState(temporal)
356
+ }
357
+ throw error
278
358
  }
279
- throw error
280
- } finally {
281
- if (!wasTracking) temporal.pause()
282
- }
359
+ })
283
360
  }
284
361
  useInteractionScope.getState().finishSubtree(rootId)
285
362
  if (usePlacementPreview.getState().node?.id === rootId) usePlacementPreview.getState().clear()
@@ -0,0 +1,157 @@
1
+ import {
2
+ type AnyNodeId,
3
+ type CompiledGeometryScript,
4
+ GEOMETRY_SCRIPT_MIME_TYPES,
5
+ GeometryArtifactMetadata,
6
+ GeometryReuseFields,
7
+ type GeometryScriptLanguage,
8
+ type GeometryScriptParamValue,
9
+ generateId,
10
+ getArtifactStore,
11
+ isScriptedNode,
12
+ runAsSingleSceneHistoryStep,
13
+ scriptedObjectMeta,
14
+ useScene,
15
+ } from '@pascal-app/core'
16
+ import {
17
+ addColumn,
18
+ addObject,
19
+ authoredObject,
20
+ editedScriptLanguage,
21
+ editedScriptParams,
22
+ rescriptOpening,
23
+ } from '@pascal-app/core/agent-operations'
24
+ import { compileGeometryScriptInWorker } from './client'
25
+
26
+ /**
27
+ * Builds a bpy module and stores its GLB and text, returning their hashes.
28
+ * Blender runs outside the browser, so the host provides this (the hosted app
29
+ * calls its Blender service); without one, bpy scripts are refused.
30
+ */
31
+ export type BlenderScriptBuilder = (input: {
32
+ code: string
33
+ params?: Record<string, GeometryScriptParamValue>
34
+ nodeId: string
35
+ metadata: GeometryArtifactMetadata
36
+ }) => Promise<CompiledGeometryScript>
37
+
38
+ let blenderBuilder: BlenderScriptBuilder | null = null
39
+
40
+ export function configureBlenderScriptBuilder(next: BlenderScriptBuilder | null): void {
41
+ blenderBuilder = next
42
+ }
43
+
44
+ /**
45
+ * The editor's compile step for `add_object`: runs the module (a three.js one
46
+ * in the worker, a bpy one through the host's Blender builder) and stores the
47
+ * GLB and the module text, so the core operation can reference both by hash.
48
+ * Without `code`, the node's stored script is rebuilt; either way an edit keeps
49
+ * the node's param values it does not override.
50
+ */
51
+ export async function compileAndStoreGeometryScript(input: {
52
+ code?: string
53
+ language?: GeometryScriptLanguage
54
+ nodeId?: string
55
+ params?: Record<string, GeometryScriptParamValue>
56
+ kind?: 'item' | 'door' | 'window' | 'column'
57
+ inputs?: Record<string, unknown>
58
+ name?: string
59
+ description?: string
60
+ category?: string
61
+ tags?: string[]
62
+ provenance?: GeometryArtifactMetadata
63
+ }): Promise<CompiledGeometryScript> {
64
+ const code = input.code ?? (await storedScript(input.nodeId))
65
+ const node = input.nodeId ? useScene.getState().nodes[input.nodeId as AnyNodeId] : undefined
66
+ const language = editedScriptLanguage(node, input)
67
+ const params = editedScriptParams(node, input.params)
68
+ const kind =
69
+ input.kind ??
70
+ (node?.type === 'door' || node?.type === 'window' || node?.type === 'column'
71
+ ? node.type
72
+ : 'item')
73
+ const nodeId = input.nodeId ?? generateId(kind)
74
+ const metadata = GeometryArtifactMetadata.parse({
75
+ ...(isScriptedNode(node) ? scriptedObjectMeta(node) : {}),
76
+ ...input.provenance,
77
+ ...GeometryReuseFields.parse(input),
78
+ kind,
79
+ })
80
+ if (language === 'bpy') {
81
+ if (!blenderBuilder) throw new Error('This editor cannot run Blender (bpy) scripts')
82
+ return { ...(await blenderBuilder({ code, params, nodeId, metadata })), nodeId }
83
+ }
84
+ const { glb, ...compiled } = await compileGeometryScriptInWorker({
85
+ code,
86
+ params,
87
+ inputs: input.inputs,
88
+ })
89
+ const context = { nodeId, metadata: { ...metadata, mount: compiled.mount } }
90
+ const store = getArtifactStore()
91
+ const [sha256] = await Promise.all([
92
+ store.put(compiled.sha256, glb, 'model/gltf-binary', context),
93
+ store.put(
94
+ compiled.script,
95
+ new TextEncoder().encode(code),
96
+ GEOMETRY_SCRIPT_MIME_TYPES.three,
97
+ context,
98
+ ),
99
+ ])
100
+ return { ...compiled, sha256, nodeId }
101
+ }
102
+
103
+ /** The module text of an authored object, read back from the artifact store. */
104
+ export async function storedScript(nodeId: string | undefined): Promise<string> {
105
+ if (!nodeId) throw new Error('Pass the code to build a new object')
106
+ const node = authoredObject(useScene.getState().nodes, nodeId)
107
+ const code = await getArtifactStore().text(node.source.script)
108
+ if (code === null) throw new Error(`The script of ${nodeId} could not be read`)
109
+ return code
110
+ }
111
+
112
+ const rebuildGeneration = new Map<string, number>()
113
+
114
+ /**
115
+ * Re-runs a scripted node's script with new param values, the inspector's
116
+ * and resize handles' path: same code, new artifact, one undo step. A slower earlier rebuild of
117
+ * the same node never overwrites a newer one.
118
+ */
119
+ export async function rebuildAuthoredObject(
120
+ nodeId: string,
121
+ params: Record<string, GeometryScriptParamValue>,
122
+ /** Where it ends up, when a resize also moves it (a side arrow keeps the opposite edge). */
123
+ position?: [number, number, number],
124
+ ): Promise<void> {
125
+ const generation = (rebuildGeneration.get(nodeId) ?? 0) + 1
126
+ rebuildGeneration.set(nodeId, generation)
127
+ const compiled = await compileAndStoreGeometryScript({ nodeId, params })
128
+ if (rebuildGeneration.get(nodeId) !== generation) return
129
+ const changes = rebuildChanges(nodeId, compiled, params, position)
130
+ runAsSingleSceneHistoryStep(useScene, () => {
131
+ for (const { id, data } of changes?.update ?? []) {
132
+ useScene.getState().updateNode(id as AnyNodeId, data)
133
+ }
134
+ })
135
+ }
136
+
137
+ /** What a rebuild changes, through the operation of the node's kind. */
138
+ function rebuildChanges(
139
+ nodeId: string,
140
+ compiled: CompiledGeometryScript,
141
+ params: Record<string, GeometryScriptParamValue>,
142
+ position: [number, number, number] | undefined,
143
+ ) {
144
+ const nodes = useScene.getState().nodes
145
+ const context = { activeLevelId: null }
146
+ switch (nodes[nodeId as AnyNodeId]?.type) {
147
+ case 'column': {
148
+ const at = position && { x: position[0], y: position[1], z: position[2] }
149
+ return addColumn(nodes, { nodeId, compiled, ...at }, context).changes
150
+ }
151
+ case 'window':
152
+ case 'door':
153
+ return rescriptOpening(nodes, { nodeId, compiled, position }, context).changes
154
+ default:
155
+ return addObject(nodes, { params, nodeId, compiled, position }, context).changes
156
+ }
157
+ }