@pascal-app/editor 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -7,6 +7,7 @@ import {
7
7
  findLevelAncestorId,
8
8
  type GeometryContext,
9
9
  getLevelDisplayName,
10
+ hidesDescendants,
10
11
  isNodeKindEnabled,
11
12
  isOperationDoorType,
12
13
  itemClipRegistry,
@@ -18,7 +19,9 @@ import {
18
19
  type WindowNode,
19
20
  type ZoneNode,
20
21
  } from '@pascal-app/core'
22
+ import { evaluateRecipe } from '@pascal-app/core/procedural-items'
21
23
  import {
24
+ decorateProceduralEmission,
22
25
  getPascalTextureRef,
23
26
  isViewerPresentationTextureBorrowed,
24
27
  poseDoorMovingParts,
@@ -37,6 +40,8 @@ import {
37
40
  } from 'three/examples/jsm/exporters/GLTFExporter.js'
38
41
  import { createExportTextureUtils, type ExportTextureUtils } from './export-texture-utils'
39
42
  import { cloneExportUserData } from './export-user-data'
43
+ import { isIfcRolePart } from './ifc-parts'
44
+ import { nodeDisplayLabel } from './node-display-label'
40
45
  import {
41
46
  type CompressedTextureDecompressor,
42
47
  decompressCanonicalNormalMaps,
@@ -140,6 +145,115 @@ export function nextFrames(): Promise<void> {
140
145
  })
141
146
  }
142
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
+
143
257
  type GltfExtrasDef = {
144
258
  extras?: Record<string, unknown>
145
259
  }
@@ -368,7 +482,10 @@ function startSceneExportPreparation(
368
482
 
369
483
  for (const [id, original] of registryEntries) {
370
484
  const node = nodes[id]
371
- if (!node) continue
485
+ if (!node) {
486
+ excludedObjects.add(original)
487
+ continue
488
+ }
372
489
  const installedPlugins = sceneState.hasExplicitPluginInstallState
373
490
  ? sceneState.installedPlugins
374
491
  : undefined
@@ -481,10 +598,18 @@ function finishSceneExportPreparation(preparation: SceneExportPreparation): GlbE
481
598
  if (clone) identityNodes.add(clone)
482
599
  }
483
600
 
484
- pruneNonRenderableMeshes(scene, identityNodes)
601
+ const warnings = pruneNonRenderableMeshes(scene, identityNodes)
485
602
  sanitizeMaterialGroups(scene, identityNodes)
486
603
  convertMaterials(scene, options.textures ?? 'embed', options.purpose ?? 'viewer')
487
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
+
488
613
  const retainedCloneByOriginal = retainedClones(scene, cloneByOriginal)
489
614
  const keepClips = options.animations
490
615
  ? options.animations === 'keep'
@@ -492,13 +617,20 @@ function finishSceneExportPreparation(preparation: SceneExportPreparation): GlbE
492
617
  const animation = keepClips
493
618
  ? bakeAnimationClips(retainedCloneByOriginal, nodes, registryEntries)
494
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
+ }
495
627
  stampIdentity(scene, retainedCloneByOriginal, nodes, animation.clipNamesByNode, registryEntries)
496
628
 
497
629
  let disposed = false
498
630
  return {
499
631
  scene,
500
632
  animations: animation.clips,
501
- warnings: [],
633
+ warnings,
502
634
  dispose: () => {
503
635
  if (disposed) return
504
636
  disposed = true
@@ -714,45 +846,33 @@ function pruneHiddenSceneNodes(
714
846
  nodes: Record<string, AnyNode>,
715
847
  registryEntries: readonly RegistryEntry[],
716
848
  ) {
717
- const visibility = new Map<string, boolean>()
718
- const declaredSiteParents = new Map<string, string>()
719
- for (const node of Object.values(nodes)) {
720
- if (node.type !== 'site' || !('children' in node) || !Array.isArray(node.children)) continue
721
- for (const childId of node.children) {
722
- const child = nodes[childId]
723
- if (child && !child.parentId && !declaredSiteParents.has(childId)) {
724
- declaredSiteParents.set(childId, node.id)
725
- }
726
- }
727
- }
849
+ const isVisible = createExportVisibility(nodes)
728
850
 
729
- const isVisible = (id: string, path: Set<string>): boolean => {
730
- const cached = visibility.get(id)
731
- if (cached !== undefined) return cached
851
+ const nodeClones = new Set<THREE.Object3D>()
852
+ for (const [, original] of registryEntries) {
853
+ const clone = cloneByOriginal.get(original)
854
+ if (clone) nodeClones.add(clone)
855
+ }
732
856
 
857
+ for (const [id, original] of registryEntries) {
858
+ if (isVisible(id)) continue
859
+ const clone = cloneByOriginal.get(original)
860
+ if (!clone) continue
733
861
  const node = nodes[id]
734
- if (!node) return true
735
- if (node.visible === false) {
736
- visibility.set(id, false)
737
- return false
862
+ if (!node || hidesDescendants(node)) {
863
+ clone.removeFromParent()
864
+ continue
738
865
  }
739
- const parentId = node.parentId || declaredSiteParents.get(id)
740
- if (!parentId || path.has(id)) {
741
- visibility.set(id, true)
742
- return true
866
+ // Drop the hidden Site's own ground fill and boundary; keep what it hosts.
867
+ for (const child of [...clone.children]) {
868
+ if (!hostsSceneNode(child, nodeClones)) child.removeFromParent()
743
869
  }
744
-
745
- path.add(id)
746
- const visible = isVisible(parentId, path)
747
- path.delete(id)
748
- visibility.set(id, visible)
749
- return visible
750
870
  }
871
+ }
751
872
 
752
- for (const [id, original] of registryEntries) {
753
- if (isVisible(id, new Set())) continue
754
- cloneByOriginal.get(original)?.removeFromParent()
755
- }
873
+ function hostsSceneNode(object: THREE.Object3D, nodeClones: Set<THREE.Object3D>): boolean {
874
+ if (nodeClones.has(object)) return true
875
+ return object.children.some((child) => hostsSceneNode(child, nodeClones))
756
876
  }
757
877
 
758
878
  function retainedClones(
@@ -889,7 +1009,16 @@ const PLACEHOLDER_MATERIAL = new THREE.MeshBasicMaterial({ visible: false })
889
1009
  */
890
1010
  function pruneNonRenderableMeshes(root: THREE.Object3D, identityNodes: Set<THREE.Object3D>) {
891
1011
  const toRemove: THREE.Object3D[] = []
1012
+ const warnings: string[] = []
892
1013
  root.traverse((object) => {
1014
+ if (toRemove.some((ancestor) => isDescendantOf(object, ancestor))) return
1015
+ if (typeof object.userData.pascalExportRefusal === 'string') {
1016
+ warnings.push(
1017
+ `Skipped ${object.name || object.userData.pascalId || 'object'}: ${object.userData.pascalExportRefusal}`,
1018
+ )
1019
+ toRemove.push(object)
1020
+ return
1021
+ }
893
1022
  if (object.userData.pascalExport === 'strip') {
894
1023
  toRemove.push(object)
895
1024
  return
@@ -948,6 +1077,7 @@ function pruneNonRenderableMeshes(root: THREE.Object3D, identityNodes: Set<THREE
948
1077
  for (const object of toRemove) {
949
1078
  object.removeFromParent()
950
1079
  }
1080
+ return warnings
951
1081
  }
952
1082
 
953
1083
  /**
@@ -1329,15 +1459,19 @@ function bakeAnimationClips(
1329
1459
  const target = cloneByOriginal.get(original)
1330
1460
  if (!node || !target) continue
1331
1461
 
1462
+ // A window or door built from a script carries its clips like an authored item.
1463
+ const scripted = (node.type === 'door' || node.type === 'window') && node.source
1332
1464
  const clip =
1333
1465
  bakeRegistryAnimationClips(node, target) ??
1334
- (node.type === 'door'
1335
- ? bakeDoorClip(id, node, target)
1336
- : node.type === 'window'
1337
- ? bakeWindowClip(id, node as WindowNode, target)
1338
- : node.type === 'item'
1339
- ? bakeItemClip(id, target)
1340
- : null)
1466
+ (scripted
1467
+ ? bakeItemClip(id, target)
1468
+ : node.type === 'door'
1469
+ ? bakeDoorClip(id, node, target)
1470
+ : node.type === 'window'
1471
+ ? bakeWindowClip(id, node as WindowNode, target)
1472
+ : node.type === 'item'
1473
+ ? bakeItemClip(id, target)
1474
+ : null)
1341
1475
 
1342
1476
  if (clip) {
1343
1477
  const nodeClips = Array.isArray(clip) ? clip : [clip]
@@ -1360,44 +1494,47 @@ function bakeRegistryAnimationClips(
1360
1494
  }
1361
1495
 
1362
1496
  /**
1363
- * Re-emit a catalog item's ambient clip (e.g. a fan's spin) onto the baked
1364
- * subtree. The source clip targets the item GLB's nodes by name (`lamp_018`);
1365
- * since every fan shares those names, we rebind each track to the specific
1366
- * cloned node's uuid so multiple fans animate independently. The clip is named
1367
- * per node (`<id>: loop`) so the baked viewer can drive each one on its own.
1497
+ * Re-emit an item's clips (a fan's spin, an authored object's motions) onto the
1498
+ * baked subtree. Source clips target the item GLB's nodes by name (`lamp_018`);
1499
+ * since every instance shares those names, we rebind each track to the
1500
+ * specific cloned node's uuid so instances animate independently. Clips are
1501
+ * named per node (`<id>: <name>`; the viewer drives `<id>: loop`).
1368
1502
  */
1369
- function bakeItemClip(id: string, itemObject: THREE.Object3D): THREE.AnimationClip | null {
1370
- const entry = itemClipRegistry.get(id)
1371
- if (!entry) return null
1503
+ function bakeItemClip(id: string, itemObject: THREE.Object3D): THREE.AnimationClip[] | null {
1504
+ const entries = itemClipRegistry.get(id)
1505
+ if (!entries?.length) return null
1372
1506
 
1373
- const tracks: THREE.KeyframeTrack[] = []
1374
1507
  // The catalog node names (e.g. "lamp_018") repeat across every instance of the
1375
1508
  // item, and the glTF export→import roundtrip rebinds clip tracks by node name —
1376
- // so a shared name would make all fans share one clip. Uniquify the targeted
1377
- // node's name per item once, then bind tracks by its (stable) uuid.
1509
+ // so a shared name would make all fans share one clip. Uniquify each targeted
1510
+ // node's name per item once (clips may share targets), then bind by uuid.
1378
1511
  const renamed = new Map<string, THREE.Object3D>()
1379
- for (const track of entry.clip.tracks) {
1380
- const dot = track.name.lastIndexOf('.')
1381
- if (dot < 0) continue
1382
- const targetName = track.name.slice(0, dot)
1383
- const property = track.name.slice(dot + 1)
1384
- let targetNode = renamed.get(targetName)
1385
- if (!targetNode) {
1386
- const found = itemObject.getObjectByName(targetName)
1387
- if (!found) continue
1388
- found.name = `${id}__${targetName}`
1389
- renamed.set(targetName, found)
1390
- targetNode = found
1512
+ const clips: THREE.AnimationClip[] = []
1513
+ for (const entry of entries) {
1514
+ const tracks: THREE.KeyframeTrack[] = []
1515
+ for (const track of entry.clip.tracks) {
1516
+ const dot = track.name.lastIndexOf('.')
1517
+ if (dot < 0) continue
1518
+ const targetName = track.name.slice(0, dot)
1519
+ const property = track.name.slice(dot + 1)
1520
+ let targetNode = renamed.get(targetName)
1521
+ if (!targetNode) {
1522
+ const found = itemObject.getObjectByName(targetName)
1523
+ if (!found) continue
1524
+ found.name = `${id}__${targetName}`
1525
+ renamed.set(targetName, found)
1526
+ targetNode = found
1527
+ }
1528
+ const retargeted = track.clone()
1529
+ retargeted.name = `${targetNode.uuid}.${property}`
1530
+ tracks.push(retargeted)
1391
1531
  }
1392
- const retargeted = track.clone()
1393
- retargeted.name = `${targetNode.uuid}.${property}`
1394
- tracks.push(retargeted)
1532
+ if (tracks.length === 0) continue
1533
+ const clip = new THREE.AnimationClip(`${id}: ${entry.name}`, entry.clip.duration, tracks)
1534
+ clip.userData = { loop: entry.loop }
1535
+ clips.push(clip)
1395
1536
  }
1396
-
1397
- if (tracks.length === 0) return null
1398
- const clip = new THREE.AnimationClip(`${id}: loop`, entry.clip.duration, tracks)
1399
- clip.userData = { loop: entry.loop }
1400
- return clip
1537
+ return clips.length > 0 ? clips : null
1401
1538
  }
1402
1539
 
1403
1540
  /**
@@ -1636,42 +1773,6 @@ function bakeWindowClip(
1636
1773
  * (e.g. `pascalSwingLeaf`, cached-material flags) leaks into glTF extras — the
1637
1774
  * file describes itself with exactly the fields a consumer needs.
1638
1775
  */
1639
- /**
1640
- * Human-readable label for a baked node, mirroring the viewer's `getNodeName`:
1641
- * an explicit name wins, items fall back to their catalog asset name, other
1642
- * kinds to a capitalized type. Levels override this with their display name.
1643
- */
1644
- function nodeDisplayLabel(node: AnyNode): string {
1645
- if (node.name) return node.name
1646
- switch (node.type) {
1647
- case 'item':
1648
- return (node as { asset?: { name?: string } }).asset?.name || 'Item'
1649
- case 'wall':
1650
- return 'Wall'
1651
- case 'door':
1652
- return 'Door'
1653
- case 'window':
1654
- return 'Window'
1655
- case 'cabinet':
1656
- case 'cabinet-module':
1657
- return 'Cabinet'
1658
- case 'slab':
1659
- return 'Slab'
1660
- case 'ceiling':
1661
- return 'Ceiling'
1662
- case 'roof':
1663
- return 'Roof'
1664
- case 'fence':
1665
- return 'Fence'
1666
- case 'column':
1667
- return 'Column'
1668
- case 'stair':
1669
- return 'Stairs'
1670
- default:
1671
- return node.type
1672
- }
1673
- }
1674
-
1675
1776
  function stampIdentity(
1676
1777
  scene: THREE.Object3D,
1677
1778
  cloneByOriginal: Map<THREE.Object3D, THREE.Object3D>,
@@ -1682,8 +1783,43 @@ function stampIdentity(
1682
1783
  scene.traverse((object) => {
1683
1784
  const presentationId = object.userData.pascalPresentationId
1684
1785
  const label = object.userData.label
1786
+ const motion = object.userData.proceduralMotion as
1787
+ | {
1788
+ nodeId: string
1789
+ partId: string
1790
+ groupId: string
1791
+ kind: 'hinge' | 'slide' | 'spin'
1792
+ clip?: string
1793
+ activeWindow?: [number, number]
1794
+ }
1795
+ | undefined
1796
+ const surfaceNodeIds = object.userData.surfaceNodeIds
1797
+ const ifcRole = object.userData.pascalIfcRole
1798
+ const ifcParts = object.userData.pascalIfcParts
1799
+ const slotId = object.userData.slotId
1685
1800
  object.userData =
1686
1801
  typeof presentationId === 'string' ? { pascalPresentationId: presentationId, label } : {}
1802
+ if (
1803
+ Array.isArray(surfaceNodeIds) &&
1804
+ surfaceNodeIds.every((id) => typeof id === 'string' && nodes[id])
1805
+ )
1806
+ object.userData.surfaceNodeIds = surfaceNodeIds
1807
+ if (typeof ifcRole === 'string') object.userData.pascalIfcRole = ifcRole
1808
+ if (Array.isArray(ifcParts) && ifcParts.every(isIfcRolePart))
1809
+ object.userData.pascalIfcParts = ifcParts
1810
+ if (typeof slotId === 'string') object.userData.slotId = slotId
1811
+ if (motion) {
1812
+ object.userData.proceduralMotion = {
1813
+ nodeId: motion.nodeId,
1814
+ partId: motion.partId,
1815
+ groupId: motion.groupId,
1816
+ kind: motion.kind,
1817
+ ...(motion.clip && clipNamesByNode.get(motion.nodeId)?.includes(motion.clip)
1818
+ ? { clip: motion.clip }
1819
+ : {}),
1820
+ ...(motion.activeWindow ? { activeWindow: motion.activeWindow } : {}),
1821
+ }
1822
+ }
1687
1823
  })
1688
1824
 
1689
1825
  for (const [id, original] of registryEntries) {
@@ -1708,21 +1844,15 @@ function stampIdentity(
1708
1844
  extras.label = getLevelDisplayName(node as LevelNode)
1709
1845
  target.visible = true
1710
1846
  }
1711
- // Only nodes that actually baked an open clip are openable. A cased opening
1712
- // (no leaf), fixed window, or static cabinet produces no clip, so it stays
1713
- // unflagged — the file never claims a part opens when nothing moves.
1714
- if (clipNamesByNode.get(id)?.some((name) => name.endsWith(': open'))) {
1715
- const clipNames = clipNamesByNode.get(id)
1716
- if (clipNames?.length) {
1717
- extras.openable = true
1718
- extras.clips = clipNames
1719
- }
1720
- }
1721
- // Items with a baked ambient clip (a fan's spin) carry the clip name but no
1722
- // `openable` flag — nothing opens; the clip just loops.
1723
- if (node.type === 'item') {
1724
- const clipNames = clipNamesByNode.get(id)
1725
- if (clipNames?.length) extras.clips = clipNames
1847
+ // Every node that baked clips lists them, whatever its kind, so the viewer
1848
+ // can play them. Only nodes that actually baked an open clip are openable. A
1849
+ // cased opening (no leaf), fixed window, or static cabinet produces no clip,
1850
+ // so it stays unflagged — the file never claims a part opens when nothing
1851
+ // moves; an ambient loop (a fan's spin) is listed without the flag.
1852
+ const clipNames = clipNamesByNode.get(id)
1853
+ if (clipNames?.length) {
1854
+ if (clipNames.some((name) => name.endsWith(': open'))) extras.openable = true
1855
+ extras.clips = clipNames
1726
1856
  }
1727
1857
  if (node.type === 'zone') {
1728
1858
  // Zone fills are stripped from the bake; /viewer rebuilds the room from
@@ -1730,6 +1860,7 @@ function stampIdentity(
1730
1860
  // `onlyVisible` keeps it even when the editor had zones hidden at export.
1731
1861
  const zone = node as ZoneNode
1732
1862
  extras.polygon = zone.polygon
1863
+ if (zone.holes?.length) extras.holes = zone.holes
1733
1864
  extras.color = zone.color
1734
1865
  target.visible = true
1735
1866
  }
@@ -1741,6 +1872,6 @@ function stampIdentity(
1741
1872
  extras.rotation = (node as { rotation?: number }).rotation ?? 0
1742
1873
  target.visible = true
1743
1874
  }
1744
- target.userData = extras
1875
+ target.userData = { ...target.userData, ...extras }
1745
1876
  }
1746
1877
  }
@@ -0,0 +1,19 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { Group, Vector3 } from 'three'
3
+ import { gridLocalNormal, gridLocalPoint } from './grid-frame'
4
+
5
+ test('grid floor and cursor stay in model coordinates after God-view pan, scale and yaw', () => {
6
+ const root = new Group()
7
+ root.position.set(2, 4, -3)
8
+ root.rotation.y = Math.PI / 3
9
+ root.scale.setScalar(0.2)
10
+ root.updateMatrixWorld(true)
11
+ const local = new Vector3(3, 2.5, 7)
12
+ const world = root.localToWorld(local.clone())
13
+ expect(gridLocalPoint(root, world, new Vector3()).distanceTo(local)).toBeLessThan(1e-8)
14
+ const normal = new Vector3(1, 0, 0).transformDirection(root.matrixWorld)
15
+ expect(
16
+ gridLocalNormal(root, normal, new Vector3()).distanceTo(new Vector3(1, 0, 0)),
17
+ ).toBeLessThan(1e-8)
18
+ expect(world.distanceTo(local)).toBeGreaterThan(1)
19
+ })
@@ -0,0 +1,18 @@
1
+ import type { Object3D, Vector3 } from 'three'
2
+
3
+ // Placement surfaces are world-space; the grid lives under the scalable XR
4
+ // scene root. Keep its lattice and reveal in that root's model coordinates.
5
+ export function gridLocalPoint(parent: Object3D | null, world: Vector3, target: Vector3) {
6
+ target.copy(world)
7
+ if (parent) {
8
+ parent.updateWorldMatrix(true, false)
9
+ parent.worldToLocal(target)
10
+ }
11
+ return target
12
+ }
13
+
14
+ export function gridLocalNormal(parent: Object3D | null, normal: Vector3, target: Vector3) {
15
+ target.copy(normal)
16
+ if (parent) target.transformDirection(parent.matrixWorld.clone().invert())
17
+ return target
18
+ }
@@ -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)
@@ -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()