@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
@@ -19,7 +19,9 @@ import {
19
19
  type WindowNode,
20
20
  type ZoneNode,
21
21
  } from '@pascal-app/core'
22
+ import { evaluateRecipe } from '@pascal-app/core/procedural-items'
22
23
  import {
24
+ decorateProceduralEmission,
23
25
  getPascalTextureRef,
24
26
  isViewerPresentationTextureBorrowed,
25
27
  poseDoorMovingParts,
@@ -38,6 +40,8 @@ import {
38
40
  } from 'three/examples/jsm/exporters/GLTFExporter.js'
39
41
  import { createExportTextureUtils, type ExportTextureUtils } from './export-texture-utils'
40
42
  import { cloneExportUserData } from './export-user-data'
43
+ import { isIfcRolePart } from './ifc-parts'
44
+ import { nodeDisplayLabel } from './node-display-label'
41
45
  import {
42
46
  type CompressedTextureDecompressor,
43
47
  decompressCanonicalNormalMaps,
@@ -141,6 +145,115 @@ export function nextFrames(): Promise<void> {
141
145
  })
142
146
  }
143
147
 
148
+ /**
149
+ * Export visibility, shared with the clone pruning: a hidden node hides its
150
+ * subtree, except a Site, which hides only its own ground (`hidesDescendants`).
151
+ */
152
+ export function createExportVisibility(nodes: Record<string, AnyNode>) {
153
+ const visibility = new Map<string, boolean>()
154
+ const isVisible = (id: string, path: Set<string> = new Set()): boolean => {
155
+ const cached = visibility.get(id)
156
+ if (cached !== undefined) return cached
157
+ const node = nodes[id]
158
+ if (!node) return true
159
+ if (node.visible === false) {
160
+ visibility.set(id, false)
161
+ return false
162
+ }
163
+ const parentId = node.parentId
164
+ const parent = parentId ? nodes[parentId] : undefined
165
+ if (!parentId || path.has(id) || (parent && !hidesDescendants(parent))) {
166
+ visibility.set(id, true)
167
+ return true
168
+ }
169
+ path.add(id)
170
+ const visible = isVisible(parentId, path)
171
+ path.delete(id)
172
+ visibility.set(id, visible)
173
+ return visible
174
+ }
175
+ return isVisible
176
+ }
177
+
178
+ export type ExportGeometryScope = Pick<GlbExportOptions, 'onlyVisible' | 'excludedNodeTypes'>
179
+
180
+ /**
181
+ * Included `bake: 'replace'` nodes whose live object holds no mesh yet. These
182
+ * kinds render collectively in the editor and mount their own geometry only
183
+ * while `isExporting` is set; kinds with bake hooks are rebuilt after the
184
+ * clone instead, and excluded or hidden nodes never reach the clone.
185
+ */
186
+ export function nodesAwaitingExportGeometry(
187
+ nodes: Record<string, AnyNode>,
188
+ scope: ExportGeometryScope = {},
189
+ ): string[] {
190
+ const sceneState = useScene.getState()
191
+ const installedPlugins = sceneState.hasExplicitPluginInstallState
192
+ ? sceneState.installedPlugins
193
+ : undefined
194
+ const excluded = new Set(scope.excludedNodeTypes)
195
+ const isVisible = createExportVisibility(nodes)
196
+ const typeExcluded = (node: AnyNode): boolean => {
197
+ for (let current: AnyNode | undefined = node, guard = 0; current && guard < 64; guard++) {
198
+ if (excluded.has(current.type)) return true
199
+ current = current.parentId ? nodes[current.parentId] : undefined
200
+ }
201
+ return false
202
+ }
203
+ const pending: string[] = []
204
+ for (const [id, object] of sceneRegistry.nodes) {
205
+ const node = nodes[id]
206
+ if (!node) continue
207
+ const definition = nodeRegistry.get(node.type)
208
+ if (definition?.bake !== 'replace' || definition.bakeGeometry || definition.bakeGeometryAsync) {
209
+ continue
210
+ }
211
+ if (!isNodeKindEnabled(node.type, installedPlugins) || typeExcluded(node)) continue
212
+ if ((scope.onlyVisible ?? true) && !isVisible(id)) continue
213
+ let hasMesh = false
214
+ object.traverse((child) => {
215
+ if ((child as THREE.Mesh).isMesh) hasMesh = true
216
+ })
217
+ if (!hasMesh) pending.push(id)
218
+ }
219
+ return pending
220
+ }
221
+
222
+ /** Two animation frames, or `ms` if frames have stopped (hidden tab, test runner). */
223
+ function nextFramesOrTimeout(ms: number): Promise<void> {
224
+ return new Promise((resolve) => {
225
+ const timer = setTimeout(resolve, ms)
226
+ if (typeof requestAnimationFrame !== 'function') return
227
+ requestAnimationFrame(() =>
228
+ requestAnimationFrame(() => {
229
+ clearTimeout(timer)
230
+ resolve()
231
+ }),
232
+ )
233
+ })
234
+ }
235
+
236
+ /**
237
+ * After `setExporting(true)`, let one commit land, then wait until every
238
+ * included replace-kind node has mounted its export geometry. Two frames are
239
+ * not enough: on a large scene with a busy main thread the proxies' re-render
240
+ * lands several frames later, and the clone would silently miss every plant.
241
+ * The deadline runs on its own timer, so stalled frames cannot hang an export.
242
+ */
243
+ export async function waitForExportGeometry(
244
+ nodes: Record<string, AnyNode>,
245
+ scope: ExportGeometryScope = {},
246
+ timeoutMs = 10_000,
247
+ ): Promise<void> {
248
+ const deadline = Date.now() + timeoutMs
249
+ await nextFramesOrTimeout(Math.min(500, timeoutMs))
250
+ while (nodesAwaitingExportGeometry(nodes, scope).length > 0) {
251
+ const remaining = deadline - Date.now()
252
+ if (remaining <= 0) return
253
+ await nextFramesOrTimeout(Math.min(50, remaining))
254
+ }
255
+ }
256
+
144
257
  type GltfExtrasDef = {
145
258
  extras?: Record<string, unknown>
146
259
  }
@@ -369,7 +482,10 @@ function startSceneExportPreparation(
369
482
 
370
483
  for (const [id, original] of registryEntries) {
371
484
  const node = nodes[id]
372
- if (!node) continue
485
+ if (!node) {
486
+ excludedObjects.add(original)
487
+ continue
488
+ }
373
489
  const installedPlugins = sceneState.hasExplicitPluginInstallState
374
490
  ? sceneState.installedPlugins
375
491
  : undefined
@@ -482,10 +598,18 @@ function finishSceneExportPreparation(preparation: SceneExportPreparation): GlbE
482
598
  if (clone) identityNodes.add(clone)
483
599
  }
484
600
 
485
- pruneNonRenderableMeshes(scene, identityNodes)
601
+ const warnings = pruneNonRenderableMeshes(scene, identityNodes)
486
602
  sanitizeMaterialGroups(scene, identityNodes)
487
603
  convertMaterials(scene, options.textures ?? 'embed', options.purpose ?? 'viewer')
488
604
 
605
+ for (const [id, original] of registryEntries) {
606
+ const node = nodes[id]
607
+ const clone = cloneByOriginal.get(original)
608
+ if (node?.type !== 'procedural-item' || !clone) continue
609
+ const lights = evaluateRecipe(node.recipe, node.parameters).lights
610
+ decorateProceduralEmission(clone, lights, true)
611
+ }
612
+
489
613
  const retainedCloneByOriginal = retainedClones(scene, cloneByOriginal)
490
614
  const keepClips = options.animations
491
615
  ? options.animations === 'keep'
@@ -493,13 +617,20 @@ function finishSceneExportPreparation(preparation: SceneExportPreparation): GlbE
493
617
  const animation = keepClips
494
618
  ? bakeAnimationClips(retainedCloneByOriginal, nodes, registryEntries)
495
619
  : { clips: [], clipNamesByNode: new Map<string, string[]>() }
620
+ if (!keepClips) {
621
+ for (const [id, original] of registryEntries) {
622
+ const node = nodes[id]
623
+ const clone = retainedCloneByOriginal.get(original)
624
+ if (node?.type === 'procedural-item' && clone) bakeRegistryAnimationClips(node, clone)
625
+ }
626
+ }
496
627
  stampIdentity(scene, retainedCloneByOriginal, nodes, animation.clipNamesByNode, registryEntries)
497
628
 
498
629
  let disposed = false
499
630
  return {
500
631
  scene,
501
632
  animations: animation.clips,
502
- warnings: [],
633
+ warnings,
503
634
  dispose: () => {
504
635
  if (disposed) return
505
636
  disposed = true
@@ -715,31 +846,7 @@ function pruneHiddenSceneNodes(
715
846
  nodes: Record<string, AnyNode>,
716
847
  registryEntries: readonly RegistryEntry[],
717
848
  ) {
718
- const visibility = new Map<string, boolean>()
719
-
720
- const isVisible = (id: string, path: Set<string>): boolean => {
721
- const cached = visibility.get(id)
722
- if (cached !== undefined) return cached
723
-
724
- const node = nodes[id]
725
- if (!node) return true
726
- if (node.visible === false) {
727
- visibility.set(id, false)
728
- return false
729
- }
730
- const parentId = node.parentId
731
- const parent = parentId ? nodes[parentId] : undefined
732
- if (!parentId || path.has(id) || (parent && !hidesDescendants(parent))) {
733
- visibility.set(id, true)
734
- return true
735
- }
736
-
737
- path.add(id)
738
- const visible = isVisible(parentId, path)
739
- path.delete(id)
740
- visibility.set(id, visible)
741
- return visible
742
- }
849
+ const isVisible = createExportVisibility(nodes)
743
850
 
744
851
  const nodeClones = new Set<THREE.Object3D>()
745
852
  for (const [, original] of registryEntries) {
@@ -748,7 +855,7 @@ function pruneHiddenSceneNodes(
748
855
  }
749
856
 
750
857
  for (const [id, original] of registryEntries) {
751
- if (isVisible(id, new Set())) continue
858
+ if (isVisible(id)) continue
752
859
  const clone = cloneByOriginal.get(original)
753
860
  if (!clone) continue
754
861
  const node = nodes[id]
@@ -796,7 +903,10 @@ function cloneSceneForExport(
796
903
  ): THREE.Object3D {
797
904
  if (excludedObjects.has(source)) return new THREE.Group()
798
905
 
799
- const clone = source.clone(false)
906
+ // LOD.copy clones every level even when called through clone(false). Export
907
+ // walks the children itself, and plugin LOD constructors may require inputs.
908
+ const clone =
909
+ source instanceof THREE.LOD ? new THREE.Group().copy(source, false) : source.clone(false)
800
910
  clone.userData = cloneExportUserData(source.userData)
801
911
  const renderable = source as THREE.Mesh
802
912
  const renderableClone = clone as THREE.Mesh
@@ -902,7 +1012,16 @@ const PLACEHOLDER_MATERIAL = new THREE.MeshBasicMaterial({ visible: false })
902
1012
  */
903
1013
  function pruneNonRenderableMeshes(root: THREE.Object3D, identityNodes: Set<THREE.Object3D>) {
904
1014
  const toRemove: THREE.Object3D[] = []
1015
+ const warnings: string[] = []
905
1016
  root.traverse((object) => {
1017
+ if (toRemove.some((ancestor) => isDescendantOf(object, ancestor))) return
1018
+ if (typeof object.userData.pascalExportRefusal === 'string') {
1019
+ warnings.push(
1020
+ `Skipped ${object.name || object.userData.pascalId || 'object'}: ${object.userData.pascalExportRefusal}`,
1021
+ )
1022
+ toRemove.push(object)
1023
+ return
1024
+ }
906
1025
  if (object.userData.pascalExport === 'strip') {
907
1026
  toRemove.push(object)
908
1027
  return
@@ -961,6 +1080,7 @@ function pruneNonRenderableMeshes(root: THREE.Object3D, identityNodes: Set<THREE
961
1080
  for (const object of toRemove) {
962
1081
  object.removeFromParent()
963
1082
  }
1083
+ return warnings
964
1084
  }
965
1085
 
966
1086
  /**
@@ -1342,15 +1462,19 @@ function bakeAnimationClips(
1342
1462
  const target = cloneByOriginal.get(original)
1343
1463
  if (!node || !target) continue
1344
1464
 
1465
+ // A window or door built from a script carries its clips like an authored item.
1466
+ const scripted = (node.type === 'door' || node.type === 'window') && node.source
1345
1467
  const clip =
1346
1468
  bakeRegistryAnimationClips(node, target) ??
1347
- (node.type === 'door'
1348
- ? bakeDoorClip(id, node, target)
1349
- : node.type === 'window'
1350
- ? bakeWindowClip(id, node as WindowNode, target)
1351
- : node.type === 'item'
1352
- ? bakeItemClip(id, target)
1353
- : null)
1469
+ (scripted
1470
+ ? bakeItemClip(id, target)
1471
+ : node.type === 'door'
1472
+ ? bakeDoorClip(id, node, target)
1473
+ : node.type === 'window'
1474
+ ? bakeWindowClip(id, node as WindowNode, target)
1475
+ : node.type === 'item'
1476
+ ? bakeItemClip(id, target)
1477
+ : null)
1354
1478
 
1355
1479
  if (clip) {
1356
1480
  const nodeClips = Array.isArray(clip) ? clip : [clip]
@@ -1373,44 +1497,47 @@ function bakeRegistryAnimationClips(
1373
1497
  }
1374
1498
 
1375
1499
  /**
1376
- * Re-emit a catalog item's ambient clip (e.g. a fan's spin) onto the baked
1377
- * subtree. The source clip targets the item GLB's nodes by name (`lamp_018`);
1378
- * since every fan shares those names, we rebind each track to the specific
1379
- * cloned node's uuid so multiple fans animate independently. The clip is named
1380
- * per node (`<id>: loop`) so the baked viewer can drive each one on its own.
1500
+ * Re-emit an item's clips (a fan's spin, an authored object's motions) onto the
1501
+ * baked subtree. Source clips target the item GLB's nodes by name (`lamp_018`);
1502
+ * since every instance shares those names, we rebind each track to the
1503
+ * specific cloned node's uuid so instances animate independently. Clips are
1504
+ * named per node (`<id>: <name>`; the viewer drives `<id>: loop`).
1381
1505
  */
1382
- function bakeItemClip(id: string, itemObject: THREE.Object3D): THREE.AnimationClip | null {
1383
- const entry = itemClipRegistry.get(id)
1384
- if (!entry) return null
1506
+ function bakeItemClip(id: string, itemObject: THREE.Object3D): THREE.AnimationClip[] | null {
1507
+ const entries = itemClipRegistry.get(id)
1508
+ if (!entries?.length) return null
1385
1509
 
1386
- const tracks: THREE.KeyframeTrack[] = []
1387
1510
  // The catalog node names (e.g. "lamp_018") repeat across every instance of the
1388
1511
  // item, and the glTF export→import roundtrip rebinds clip tracks by node name —
1389
- // so a shared name would make all fans share one clip. Uniquify the targeted
1390
- // node's name per item once, then bind tracks by its (stable) uuid.
1512
+ // so a shared name would make all fans share one clip. Uniquify each targeted
1513
+ // node's name per item once (clips may share targets), then bind by uuid.
1391
1514
  const renamed = new Map<string, THREE.Object3D>()
1392
- for (const track of entry.clip.tracks) {
1393
- const dot = track.name.lastIndexOf('.')
1394
- if (dot < 0) continue
1395
- const targetName = track.name.slice(0, dot)
1396
- const property = track.name.slice(dot + 1)
1397
- let targetNode = renamed.get(targetName)
1398
- if (!targetNode) {
1399
- const found = itemObject.getObjectByName(targetName)
1400
- if (!found) continue
1401
- found.name = `${id}__${targetName}`
1402
- renamed.set(targetName, found)
1403
- targetNode = found
1515
+ const clips: THREE.AnimationClip[] = []
1516
+ for (const entry of entries) {
1517
+ const tracks: THREE.KeyframeTrack[] = []
1518
+ for (const track of entry.clip.tracks) {
1519
+ const dot = track.name.lastIndexOf('.')
1520
+ if (dot < 0) continue
1521
+ const targetName = track.name.slice(0, dot)
1522
+ const property = track.name.slice(dot + 1)
1523
+ let targetNode = renamed.get(targetName)
1524
+ if (!targetNode) {
1525
+ const found = itemObject.getObjectByName(targetName)
1526
+ if (!found) continue
1527
+ found.name = `${id}__${targetName}`
1528
+ renamed.set(targetName, found)
1529
+ targetNode = found
1530
+ }
1531
+ const retargeted = track.clone()
1532
+ retargeted.name = `${targetNode.uuid}.${property}`
1533
+ tracks.push(retargeted)
1404
1534
  }
1405
- const retargeted = track.clone()
1406
- retargeted.name = `${targetNode.uuid}.${property}`
1407
- tracks.push(retargeted)
1535
+ if (tracks.length === 0) continue
1536
+ const clip = new THREE.AnimationClip(`${id}: ${entry.name}`, entry.clip.duration, tracks)
1537
+ clip.userData = { loop: entry.loop }
1538
+ clips.push(clip)
1408
1539
  }
1409
-
1410
- if (tracks.length === 0) return null
1411
- const clip = new THREE.AnimationClip(`${id}: loop`, entry.clip.duration, tracks)
1412
- clip.userData = { loop: entry.loop }
1413
- return clip
1540
+ return clips.length > 0 ? clips : null
1414
1541
  }
1415
1542
 
1416
1543
  /**
@@ -1649,42 +1776,6 @@ function bakeWindowClip(
1649
1776
  * (e.g. `pascalSwingLeaf`, cached-material flags) leaks into glTF extras — the
1650
1777
  * file describes itself with exactly the fields a consumer needs.
1651
1778
  */
1652
- /**
1653
- * Human-readable label for a baked node, mirroring the viewer's `getNodeName`:
1654
- * an explicit name wins, items fall back to their catalog asset name, other
1655
- * kinds to a capitalized type. Levels override this with their display name.
1656
- */
1657
- function nodeDisplayLabel(node: AnyNode): string {
1658
- if (node.name) return node.name
1659
- switch (node.type) {
1660
- case 'item':
1661
- return (node as { asset?: { name?: string } }).asset?.name || 'Item'
1662
- case 'wall':
1663
- return 'Wall'
1664
- case 'door':
1665
- return 'Door'
1666
- case 'window':
1667
- return 'Window'
1668
- case 'cabinet':
1669
- case 'cabinet-module':
1670
- return 'Cabinet'
1671
- case 'slab':
1672
- return 'Slab'
1673
- case 'ceiling':
1674
- return 'Ceiling'
1675
- case 'roof':
1676
- return 'Roof'
1677
- case 'fence':
1678
- return 'Fence'
1679
- case 'column':
1680
- return 'Column'
1681
- case 'stair':
1682
- return 'Stairs'
1683
- default:
1684
- return node.type
1685
- }
1686
- }
1687
-
1688
1779
  function stampIdentity(
1689
1780
  scene: THREE.Object3D,
1690
1781
  cloneByOriginal: Map<THREE.Object3D, THREE.Object3D>,
@@ -1695,8 +1786,43 @@ function stampIdentity(
1695
1786
  scene.traverse((object) => {
1696
1787
  const presentationId = object.userData.pascalPresentationId
1697
1788
  const label = object.userData.label
1789
+ const motion = object.userData.proceduralMotion as
1790
+ | {
1791
+ nodeId: string
1792
+ partId: string
1793
+ groupId: string
1794
+ kind: 'hinge' | 'slide' | 'spin'
1795
+ clip?: string
1796
+ activeWindow?: [number, number]
1797
+ }
1798
+ | undefined
1799
+ const surfaceNodeIds = object.userData.surfaceNodeIds
1800
+ const ifcRole = object.userData.pascalIfcRole
1801
+ const ifcParts = object.userData.pascalIfcParts
1802
+ const slotId = object.userData.slotId
1698
1803
  object.userData =
1699
1804
  typeof presentationId === 'string' ? { pascalPresentationId: presentationId, label } : {}
1805
+ if (
1806
+ Array.isArray(surfaceNodeIds) &&
1807
+ surfaceNodeIds.every((id) => typeof id === 'string' && nodes[id])
1808
+ )
1809
+ object.userData.surfaceNodeIds = surfaceNodeIds
1810
+ if (typeof ifcRole === 'string') object.userData.pascalIfcRole = ifcRole
1811
+ if (Array.isArray(ifcParts) && ifcParts.every(isIfcRolePart))
1812
+ object.userData.pascalIfcParts = ifcParts
1813
+ if (typeof slotId === 'string') object.userData.slotId = slotId
1814
+ if (motion) {
1815
+ object.userData.proceduralMotion = {
1816
+ nodeId: motion.nodeId,
1817
+ partId: motion.partId,
1818
+ groupId: motion.groupId,
1819
+ kind: motion.kind,
1820
+ ...(motion.clip && clipNamesByNode.get(motion.nodeId)?.includes(motion.clip)
1821
+ ? { clip: motion.clip }
1822
+ : {}),
1823
+ ...(motion.activeWindow ? { activeWindow: motion.activeWindow } : {}),
1824
+ }
1825
+ }
1700
1826
  })
1701
1827
 
1702
1828
  for (const [id, original] of registryEntries) {
@@ -1721,21 +1847,15 @@ function stampIdentity(
1721
1847
  extras.label = getLevelDisplayName(node as LevelNode)
1722
1848
  target.visible = true
1723
1849
  }
1724
- // Only nodes that actually baked an open clip are openable. A cased opening
1725
- // (no leaf), fixed window, or static cabinet produces no clip, so it stays
1726
- // unflagged — the file never claims a part opens when nothing moves.
1727
- if (clipNamesByNode.get(id)?.some((name) => name.endsWith(': open'))) {
1728
- const clipNames = clipNamesByNode.get(id)
1729
- if (clipNames?.length) {
1730
- extras.openable = true
1731
- extras.clips = clipNames
1732
- }
1733
- }
1734
- // Items with a baked ambient clip (a fan's spin) carry the clip name but no
1735
- // `openable` flag — nothing opens; the clip just loops.
1736
- if (node.type === 'item') {
1737
- const clipNames = clipNamesByNode.get(id)
1738
- if (clipNames?.length) extras.clips = clipNames
1850
+ // Every node that baked clips lists them, whatever its kind, so the viewer
1851
+ // can play them. Only nodes that actually baked an open clip are openable. A
1852
+ // cased opening (no leaf), fixed window, or static cabinet produces no clip,
1853
+ // so it stays unflagged — the file never claims a part opens when nothing
1854
+ // moves; an ambient loop (a fan's spin) is listed without the flag.
1855
+ const clipNames = clipNamesByNode.get(id)
1856
+ if (clipNames?.length) {
1857
+ if (clipNames.some((name) => name.endsWith(': open'))) extras.openable = true
1858
+ extras.clips = clipNames
1739
1859
  }
1740
1860
  if (node.type === 'zone') {
1741
1861
  // Zone fills are stripped from the bake; /viewer rebuilds the room from
@@ -1743,6 +1863,7 @@ function stampIdentity(
1743
1863
  // `onlyVisible` keeps it even when the editor had zones hidden at export.
1744
1864
  const zone = node as ZoneNode
1745
1865
  extras.polygon = zone.polygon
1866
+ if (zone.holes?.length) extras.holes = zone.holes
1746
1867
  extras.color = zone.color
1747
1868
  target.visible = true
1748
1869
  }
@@ -1754,6 +1875,6 @@ function stampIdentity(
1754
1875
  extras.rotation = (node as { rotation?: number }).rotation ?? 0
1755
1876
  target.visible = true
1756
1877
  }
1757
- target.userData = extras
1878
+ target.userData = { ...target.userData, ...extras }
1758
1879
  }
1759
1880
  }
@@ -109,11 +109,10 @@ describe('standalone history source invalidation', () => {
109
109
  `)
110
110
  })
111
111
 
112
- test.each([
113
- true,
114
- false,
115
- ])('discarded preview neighbours rebuild after undo/redo (joined: %s)', (joined) => {
116
- runSourceHistoryTest(`
112
+ test.each([true, false])(
113
+ 'discarded preview neighbours rebuild after undo/redo (joined: %s)',
114
+ (joined) => {
115
+ runSourceHistoryTest(`
117
116
  const react = await importShared('react')
118
117
  mockShared('react', () => ({ ...react, useEffect: () => {} }))
119
118
  const frames = []
@@ -152,7 +151,8 @@ describe('standalone history source invalidation', () => {
152
151
  assert.deepEqual(Array.from(b.geometry.getAttribute('position').array), canonical)
153
152
  }
154
153
  `)
155
- })
154
+ },
155
+ )
156
156
 
157
157
  test('opening reparent dirties old and new walls on undo and redo', () => {
158
158
  runSourceHistoryTest(`
@@ -217,6 +217,9 @@ describe('standalone history source invalidation', () => {
217
217
  const { Group, Mesh, MeshBasicMaterial, BoxGeometry } = await importShared('three')
218
218
  const viewer = await importShared('@pascal-app/viewer')
219
219
  const { captureChangedNodes, runBatchFrame, resetNodeBatchState } = await import(${JSON.stringify(resolve(import.meta.dir, '../../../nodes/src/shared/node-batch/system.tsx'))})
220
+ // The node batch reads each kind's \`capabilities.batchable\` from the registry.
221
+ const { doorBatchable } = await import(${JSON.stringify(resolve(import.meta.dir, '../../../nodes/src/shared/node-batch/batchable.ts'))})
222
+ core.nodeRegistry._register({ kind: 'door', schemaVersion: 1, capabilities: { batchable: doorBatchable } })
220
223
  const root = new Group()
221
224
  core.sceneRegistry.nodes.set(level.id, root)
222
225
  core.sceneRegistry.byType.level.add(level.id)
@@ -260,6 +263,9 @@ describe('standalone history source invalidation', () => {
260
263
  const { Group, Mesh, MeshBasicMaterial, BoxGeometry } = await importShared('three')
261
264
  const viewer = await importShared('@pascal-app/viewer')
262
265
  const { captureChangedNodes, runBatchFrame, resetNodeBatchState } = await import(${JSON.stringify(resolve(import.meta.dir, '../../../nodes/src/shared/node-batch/system.tsx'))})
266
+ // The node batch reads each kind's \`capabilities.batchable\` from the registry.
267
+ const { doorBatchable } = await import(${JSON.stringify(resolve(import.meta.dir, '../../../nodes/src/shared/node-batch/batchable.ts'))})
268
+ core.nodeRegistry._register({ kind: 'door', schemaVersion: 1, capabilities: { batchable: doorBatchable } })
263
269
  const root = new Group()
264
270
  core.sceneRegistry.nodes.set(level.id, root)
265
271
  core.sceneRegistry.byType.level.add(level.id)
@@ -337,7 +343,12 @@ describe('standalone history source invalidation', () => {
337
343
  mockShared('react', () => ({ ...react, useEffect: () => {}, useRef: current => ({ current }) }))
338
344
  const frames = []
339
345
  const fiber = await importShared('@react-three/fiber')
340
- mockShared('@react-three/fiber', () => ({ ...fiber, useFrame: frame => frames.push(frame) }))
346
+ // DoorSystem and WindowSystem register their materials for scene capture through useThree.
347
+ mockShared('@react-three/fiber', () => ({
348
+ ...fiber,
349
+ useFrame: frame => frames.push(frame),
350
+ useThree: select => select({ scene: {} }),
351
+ }))
341
352
  const selector = store => Object.assign(fn => fn(store.getState()), store)
342
353
  mockShared('@pascal-app/core', () => ({ ...core, useScene: selector(scene), useLiveNodeOverrides: selector(overrides) }))
343
354
  const viewer = await importShared('@pascal-app/viewer')
@@ -9,6 +9,7 @@ import {
9
9
  } from '@pascal-app/core'
10
10
  import { markPerfAction } from '@pascal-app/viewer'
11
11
  import useInteractionScope from '../store/use-interaction-scope'
12
+ import { cancelGestures } from './gesture-lifecycle'
12
13
  import { registeredDraftingConfig } from './interaction/registered-drafting'
13
14
 
14
15
  export type HistoryCommandState = {
@@ -130,6 +131,9 @@ export function shouldCancelDraftOnHistoryJump(): boolean {
130
131
  }
131
132
 
132
133
  export function runUndo(): HistoryCommandResult {
134
+ // A history jump under a live gesture would land on its paused baseline;
135
+ // every history command, not only the keyboard's, ends it first.
136
+ cancelGestures('history')
133
137
  if (shouldCancelDraftOnHistoryJump()) emitter.emit('tool:cancel')
134
138
  if (historyCommandDelegate) {
135
139
  const result = historyCommandDelegate.undo()
@@ -147,6 +151,9 @@ export function runUndo(): HistoryCommandResult {
147
151
  }
148
152
 
149
153
  export function runRedo(): HistoryCommandResult {
154
+ // A history jump under a live gesture would land on its paused baseline;
155
+ // every history command, not only the keyboard's, ends it first.
156
+ cancelGestures('history')
150
157
  if (shouldCancelDraftOnHistoryJump()) emitter.emit('tool:cancel')
151
158
  if (historyCommandDelegate) {
152
159
  const result = historyCommandDelegate.redo()