@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
@@ -1,16 +1,29 @@
1
1
  import {
2
2
  type AnyNode,
3
3
  type AnyNodeId,
4
+ beginSceneHistoryPauseSession,
5
+ type CloneNodesIntoResult,
4
6
  cloneNodesInto,
7
+ collectionIdsOf,
5
8
  collectSubtree,
9
+ createSceneApi,
6
10
  type DuplicableConfig,
11
+ getSceneHistoryPauseDepth,
12
+ getSurfaceProvider,
7
13
  nodeRegistry,
14
+ resolveSurfacePlacement,
15
+ runSceneHistoryDraftWrite,
16
+ type SurfaceRejectReason,
8
17
  useScene,
9
18
  } from '@pascal-app/core'
19
+ import { evaluateRecipe, isProceduralItem } from '@pascal-app/core/procedural-items'
20
+ import useInteractionScope from '../store/use-interaction-scope'
21
+ import usePlacementPreview from '../store/use-placement-preview'
10
22
  import { getPlacementMetadataRecord, stripPlacementMetadataFlags } from './placement-metadata'
11
23
  import {
12
24
  surfaceAttachmentId,
13
25
  surfaceAttachmentUpdates,
26
+ surfaceFramePose,
14
27
  updateSurfaceNode,
15
28
  } from './surface-attachment'
16
29
 
@@ -26,13 +39,33 @@ function parentIdOf(node: AnyNode): AnyNodeId | undefined {
26
39
  return parentId ?? undefined
27
40
  }
28
41
 
42
+ /**
43
+ * Create ops for a clone of scene nodes: each copy joins the collections its
44
+ * source is in, whatever its kind.
45
+ */
46
+ export function copyCreateOps(cloned: CloneNodesIntoResult, parentId: AnyNodeId | undefined) {
47
+ const collections = useScene.getState().collections
48
+ const sourceIds = new Map([...cloned.idMap].map(([source, copy]) => [copy, source]))
49
+ return cloned.nodes.map((node, index) => ({
50
+ node,
51
+ ...(index === 0 && parentId ? { parentId } : {}),
52
+ collectionIds: collectionIdsOf(collections, sourceIds.get(node.id as AnyNodeId)!),
53
+ }))
54
+ }
55
+
29
56
  function duplicableConfigFor(node: AnyNode): DuplicableConfig | null {
30
57
  const duplicable = nodeRegistry.get(node.type)?.capabilities?.duplicable
31
58
  return duplicable && typeof duplicable === 'object' ? duplicable : null
32
59
  }
33
60
 
34
61
  export function duplicatesAsFreshSubtree(node: AnyNode): boolean {
35
- return duplicableConfigFor(node)?.subtree === true
62
+ // A surface-local root needs the same attachment lifecycle even without descendants.
63
+ if (surfaceAttachmentId(node) !== null) return true
64
+ const policy = duplicableConfigFor(node)?.subtree
65
+ return (
66
+ policy === true ||
67
+ (policy === 'with-children' && 'children' in node && node.children.length > 0)
68
+ )
36
69
  }
37
70
 
38
71
  /**
@@ -99,57 +132,179 @@ export function createFreshPlacementSubtree(
99
132
  parentId,
100
133
  })
101
134
 
102
- useScene
103
- .getState()
104
- .createNodes(
105
- cloned.nodes.map((node, index) => (index === 0 && parentId ? { node, parentId } : { node })),
106
- )
135
+ scene.applyNodeChanges({
136
+ create: copyCreateOps(cloned, parentId),
137
+ update: surfaceAttachmentUpdates(cloned.rootId, parentId, surfaceAttachmentId(subtree.root)),
138
+ })
107
139
 
140
+ const created = useScene.getState().nodes[cloned.rootId]
141
+ if (!created) return null
142
+ useInteractionScope.getState().noteSubtreeCreation(created)
108
143
  return cloned.rootId
109
144
  }
110
145
 
146
+ export function discardFreshPlacementSubtree(rootId: AnyNodeId): void {
147
+ const scene = useScene.getState()
148
+ if (!scene.nodes[rootId]) return
149
+ scene.applyNodeChanges({
150
+ delete: [rootId],
151
+ update: surfaceAttachmentUpdates(rootId, null, null),
152
+ })
153
+ }
154
+
155
+ function namedSurfaceRejection(
156
+ root: AnyNode,
157
+ surfaceId: string | null,
158
+ ): SurfaceRejectReason | null {
159
+ if (surfaceId === null) return null
160
+ const scene = createSceneApi(useScene)
161
+ const host = root.parentId ? scene.get(root.parentId as AnyNodeId) : undefined
162
+ if (!host || !('position' in root) || !Array.isArray(root.rotation)) return 'no-surface'
163
+ const surface = getSurfaceProvider(host)
164
+ .surfaces?.(host, { scene })
165
+ .find((s) => s.id === surfaceId)
166
+ if (!surface) return 'no-surface'
167
+ const pose = surfaceFramePose(
168
+ root.parentId,
169
+ surfaceId,
170
+ root as {
171
+ position: [number, number, number]
172
+ rotation: [number, number, number]
173
+ },
174
+ false,
175
+ )
176
+ const capabilities = nodeRegistry.get(root.type)?.capabilities
177
+ const evaluated = isProceduralItem(root) ? evaluateRecipe(root.recipe, root.parameters) : null
178
+ const bounds =
179
+ capabilities?.dragBounds?.(root, scene.nodes()) ??
180
+ (evaluated
181
+ ? {
182
+ size: evaluated.dimensions,
183
+ center: evaluated.min.map((v, i) => (v + evaluated.max[i]!) / 2) as [
184
+ number,
185
+ number,
186
+ number,
187
+ ],
188
+ }
189
+ : undefined)
190
+ const size = bounds?.size ??
191
+ capabilities?.floorPlaced?.footprint?.(root, { nodes: scene.nodes() }).dimensions ?? [0, 0, 0]
192
+ const center = bounds?.center
193
+ const localBounds = center
194
+ ? {
195
+ min: center.map((value, axis) => value - size[axis]! / 2) as [number, number, number],
196
+ max: center.map((value, axis) => value + size[axis]! / 2) as [number, number, number],
197
+ }
198
+ : undefined
199
+ let reason: SurfaceRejectReason | null = null
200
+ const placement = resolveSurfacePlacement({
201
+ host,
202
+ surface,
203
+ childKind: root.type,
204
+ childId: root.id,
205
+ childFootprint: {
206
+ size,
207
+ rotationY: pose.rotation[1],
208
+ rotation: pose.rotation,
209
+ localBounds,
210
+ },
211
+ hit: { point: pose.position, normalWorldY: 1 },
212
+ origin: pose.position,
213
+ scene,
214
+ onReject: (value) => {
215
+ reason = value
216
+ },
217
+ })
218
+ if (!placement) return reason ?? 'no-surface'
219
+ return null
220
+ }
221
+
222
+ /**
223
+ * Runs a placement's one committing write as a history step. The moving node's gesture
224
+ * sessions (the 2D move overlay's) are lifted for it; any other owner's pause still holds, so
225
+ * the write then records nothing. A legacy caller's raw `temporal.pause()` is kept afterwards.
226
+ */
227
+ function recordPlacementStep(rootId: AnyNodeId, wasTracking: boolean, write: () => void): void {
228
+ const step = beginSceneHistoryPauseSession(useScene, { gesture: rootId })
229
+ try {
230
+ step.commitStep(write)
231
+ } finally {
232
+ step.end()
233
+ if (!wasTracking && getSceneHistoryPauseDepth() === 0) useScene.temporal.getState().pause()
234
+ }
235
+ }
236
+
111
237
  /**
112
- * Finalises a fresh catalog/duplicate draft as a single undoable creation.
113
- *
114
- * Fresh drafts already exist in the scene so renderers and move tools can
115
- * preview real geometry. On commit we delete that draft while history is
116
- * paused, then create a clean clone at the final cursor position with history
117
- * resumed. Undo therefore removes the placed node instead of resurrecting the
118
- * hidden draft at its origin.
238
+ * Replace the draft in one validated write. History already excludes fresh
239
+ * subtrees, so this records one creation without first deleting the preview.
119
240
  */
120
241
  export function commitFreshPlacementSubtree(
121
242
  rootId: AnyNodeId,
122
243
  rootPatch: Partial<AnyNode>,
244
+ onReject?: (reason: SurfaceRejectReason) => void,
123
245
  ): AnyNodeId | null {
124
246
  const scene = useScene.getState()
125
247
  const subtree = collectSubtree(scene.nodes, rootId)
126
- if (!subtree) return null
248
+ if (!subtree || scene.readOnly) return null
127
249
 
128
- const root = cleanPlacementMetadata({
129
- ...subtree.root,
130
- ...rootPatch,
131
- } as AnyNode)
250
+ const root = cleanPlacementMetadata({ ...subtree.root, ...rootPatch } as AnyNode)
251
+ const surfaceId =
252
+ root.parentId === subtree.root.parentId ? surfaceAttachmentId(subtree.root) : null
253
+ const rejection = namedSurfaceRejection(root, surfaceId)
254
+ if (rejection) {
255
+ onReject?.(rejection)
256
+ return null
257
+ }
132
258
  const descendants = subtree.descendants.map((node) => cleanPlacementMetadata(node))
133
259
  const parentId = parentIdOf(root)
134
- const cloned = cloneNodesInto([root, ...descendants], {
135
- rootId,
136
- parentId,
137
- })
260
+ const cloned = cloneNodesInto([root, ...descendants], { rootId, parentId })
261
+ // The drafts' memberships, read before they are deleted.
262
+ const create = copyCreateOps(cloned, parentId)
263
+ const updates = surfaceAttachmentUpdates(rootId, null, null)
264
+ for (const update of surfaceAttachmentUpdates(cloned.rootId, parentId, surfaceId)) {
265
+ const attachments = { ...(update.data as { attachments: Record<string, string> }).attachments }
266
+ delete attachments[rootId]
267
+ const previous = updates.findIndex((entry) => entry.id === update.id)
268
+ const merged = { id: update.id, data: { attachments } }
269
+ if (previous === -1) updates.push(merged)
270
+ else updates[previous] = merged
271
+ }
138
272
 
139
- const surfaceId = surfaceAttachmentId(subtree.root)
273
+ const scope = useInteractionScope.getState()
274
+ const factoryCreated =
275
+ scope.ownedSubtree?.creation.rootId === rootId || scope.pendingSubtree?.rootId === rootId
140
276
  const temporal = useScene.temporal.getState()
141
- const wasTracking = (temporal as { isTracking?: boolean }).isTracking !== false
142
- if (wasTracking) temporal.pause()
143
- updateSurfaceNode(rootId, {}, null)
144
- useScene.getState().deleteNode(rootId)
145
- temporal.resume()
146
- useScene.getState().applyNodeChanges({
147
- create: cloned.nodes.map((node, index) =>
148
- index === 0 && parentId ? { node, parentId } : { node },
149
- ),
150
- update: surfaceAttachmentUpdates(cloned.rootId, parentId, surfaceId),
151
- })
152
- if (!wasTracking) temporal.pause()
153
-
277
+ const wasTracking = temporal.isTracking
278
+ if (!factoryCreated && descendants.length === 0 && surfaceId === null) {
279
+ // Preserve the established root-only lifecycle for placements outside the subtree factory.
280
+ runSceneHistoryDraftWrite(() => {
281
+ updateSurfaceNode(rootId, {}, null)
282
+ useScene.getState().deleteNode(rootId)
283
+ })
284
+ recordPlacementStep(rootId, wasTracking, () =>
285
+ useScene.getState().applyNodeChanges({ create, update: updates }),
286
+ )
287
+ } else {
288
+ recordPlacementStep(rootId, wasTracking, () => {
289
+ try {
290
+ // applyNodeChanges validates the complete proposed graph before publishing any part of it.
291
+ scene.applyNodeChanges({ delete: [rootId], create, update: updates })
292
+ for (const node of [subtree.root, ...subtree.descendants]) scene.clearDirty(node.id)
293
+ } catch (error) {
294
+ // Zustand publishes before notifying subscribers. A subscriber error must restore
295
+ // the draft and its ownership/history, never masquerade as a placement refusal.
296
+ useScene.temporal.getState().pause()
297
+ try {
298
+ if (useScene.getState() !== scene) useScene.setState(scene, true)
299
+ } finally {
300
+ useInteractionScope.setState(scope)
301
+ useScene.temporal.setState(temporal)
302
+ }
303
+ throw error
304
+ }
305
+ })
306
+ }
307
+ useInteractionScope.getState().finishSubtree(rootId)
308
+ if (usePlacementPreview.getState().node?.id === rootId) usePlacementPreview.getState().clear()
154
309
  return cloned.rootId
155
310
  }
@@ -0,0 +1,152 @@
1
+ import {
2
+ type AnyNodeId,
3
+ type CompiledGeometryScript,
4
+ GEOMETRY_SCRIPT_MIME_TYPES,
5
+ GeometryArtifactMetadata,
6
+ GeometryReuseFields,
7
+ type GeometryScriptLanguage,
8
+ type GeometryScriptParamValue,
9
+ generateId,
10
+ getArtifactStore,
11
+ isScriptedNode,
12
+ runAsSingleSceneHistoryStep,
13
+ scriptedObjectMeta,
14
+ useScene,
15
+ } from '@pascal-app/core'
16
+ import {
17
+ addColumn,
18
+ addObject,
19
+ authoredObject,
20
+ editedScriptLanguage,
21
+ editedScriptParams,
22
+ rescriptOpening,
23
+ } from '@pascal-app/core/agent-operations'
24
+ import { compileGeometryScriptInWorker } from './client'
25
+
26
+ /**
27
+ * Builds a bpy module and stores its GLB and text, returning their hashes.
28
+ * Blender runs outside the browser, so the host provides this (the hosted app
29
+ * calls its Blender service); without one, bpy scripts are refused.
30
+ */
31
+ export type BlenderScriptBuilder = (input: {
32
+ code: string
33
+ params?: Record<string, GeometryScriptParamValue>
34
+ nodeId: string
35
+ metadata: GeometryArtifactMetadata
36
+ }) => Promise<CompiledGeometryScript>
37
+
38
+ let blenderBuilder: BlenderScriptBuilder | null = null
39
+
40
+ export function configureBlenderScriptBuilder(next: BlenderScriptBuilder | null): void {
41
+ blenderBuilder = next
42
+ }
43
+
44
+ /**
45
+ * The editor's compile step for `add_object`: runs the module (a three.js one
46
+ * in the worker, a bpy one through the host's Blender builder) and stores the
47
+ * GLB and the module text, so the core operation can reference both by hash.
48
+ * Without `code`, the node's stored script is rebuilt; either way an edit keeps
49
+ * the node's param values it does not override.
50
+ */
51
+ export async function compileAndStoreGeometryScript(input: {
52
+ code?: string
53
+ language?: GeometryScriptLanguage
54
+ nodeId?: string
55
+ params?: Record<string, GeometryScriptParamValue>
56
+ kind?: 'item' | 'door' | 'window' | 'column'
57
+ name?: string
58
+ description?: string
59
+ category?: string
60
+ tags?: string[]
61
+ provenance?: GeometryArtifactMetadata
62
+ }): Promise<CompiledGeometryScript> {
63
+ const code = input.code ?? (await storedScript(input.nodeId))
64
+ const node = input.nodeId ? useScene.getState().nodes[input.nodeId as AnyNodeId] : undefined
65
+ const language = editedScriptLanguage(node, input)
66
+ const params = editedScriptParams(node, input.params)
67
+ const kind =
68
+ input.kind ??
69
+ (node?.type === 'door' || node?.type === 'window' || node?.type === 'column'
70
+ ? node.type
71
+ : 'item')
72
+ const nodeId = input.nodeId ?? generateId(kind)
73
+ const metadata = GeometryArtifactMetadata.parse({
74
+ ...(isScriptedNode(node) ? scriptedObjectMeta(node) : {}),
75
+ ...input.provenance,
76
+ ...GeometryReuseFields.parse(input),
77
+ kind,
78
+ })
79
+ if (language === 'bpy') {
80
+ if (!blenderBuilder) throw new Error('This editor cannot run Blender (bpy) scripts')
81
+ return { ...(await blenderBuilder({ code, params, nodeId, metadata })), nodeId }
82
+ }
83
+ const { glb, ...compiled } = await compileGeometryScriptInWorker({ code, params })
84
+ const context = { nodeId, metadata: { ...metadata, mount: compiled.mount } }
85
+ const store = getArtifactStore()
86
+ const [sha256] = await Promise.all([
87
+ store.put(compiled.sha256, glb, 'model/gltf-binary', context),
88
+ store.put(
89
+ compiled.script,
90
+ new TextEncoder().encode(code),
91
+ GEOMETRY_SCRIPT_MIME_TYPES.three,
92
+ context,
93
+ ),
94
+ ])
95
+ return { ...compiled, sha256, nodeId }
96
+ }
97
+
98
+ /** The module text of an authored object, read back from the artifact store. */
99
+ export async function storedScript(nodeId: string | undefined): Promise<string> {
100
+ if (!nodeId) throw new Error('Pass the code to build a new object')
101
+ const node = authoredObject(useScene.getState().nodes, nodeId)
102
+ const code = await getArtifactStore().text(node.source.script)
103
+ if (code === null) throw new Error(`The script of ${nodeId} could not be read`)
104
+ return code
105
+ }
106
+
107
+ const rebuildGeneration = new Map<string, number>()
108
+
109
+ /**
110
+ * Re-runs a scripted node's script with new param values, the inspector's
111
+ * and resize handles' path: same code, new artifact, one undo step. A slower earlier rebuild of
112
+ * the same node never overwrites a newer one.
113
+ */
114
+ export async function rebuildAuthoredObject(
115
+ nodeId: string,
116
+ params: Record<string, GeometryScriptParamValue>,
117
+ /** Where it ends up, when a resize also moves it (a side arrow keeps the opposite edge). */
118
+ position?: [number, number, number],
119
+ ): Promise<void> {
120
+ const generation = (rebuildGeneration.get(nodeId) ?? 0) + 1
121
+ rebuildGeneration.set(nodeId, generation)
122
+ const compiled = await compileAndStoreGeometryScript({ nodeId, params })
123
+ if (rebuildGeneration.get(nodeId) !== generation) return
124
+ const changes = rebuildChanges(nodeId, compiled, params, position)
125
+ runAsSingleSceneHistoryStep(useScene, () => {
126
+ for (const { id, data } of changes?.update ?? []) {
127
+ useScene.getState().updateNode(id as AnyNodeId, data)
128
+ }
129
+ })
130
+ }
131
+
132
+ /** What a rebuild changes, through the operation of the node's kind. */
133
+ function rebuildChanges(
134
+ nodeId: string,
135
+ compiled: CompiledGeometryScript,
136
+ params: Record<string, GeometryScriptParamValue>,
137
+ position: [number, number, number] | undefined,
138
+ ) {
139
+ const nodes = useScene.getState().nodes
140
+ const context = { activeLevelId: null }
141
+ switch (nodes[nodeId as AnyNodeId]?.type) {
142
+ case 'column': {
143
+ const at = position && { x: position[0], y: position[1], z: position[2] }
144
+ return addColumn(nodes, { nodeId, compiled, ...at }, context).changes
145
+ }
146
+ case 'window':
147
+ case 'door':
148
+ return rescriptOpening(nodes, { nodeId, compiled, position }, context).changes
149
+ default:
150
+ return addObject(nodes, { params, nodeId, compiled, position }, context).changes
151
+ }
152
+ }
@@ -0,0 +1,152 @@
1
+ import type { GeometryScriptParamValue } from '@pascal-app/core'
2
+ import type { GeometryScriptCompileOutput } from '@pascal-app/geometry-script'
3
+ import type { GeometryScriptWorkerRequest, GeometryScriptWorkerResponse } from './protocol'
4
+
5
+ const COMPILE_TIMEOUT_MS = 20_000
6
+ const START_TIMEOUT_MS = 15_000
7
+
8
+ // Model-written code runs in a worker inside a sandboxed frame: an opaque
9
+ // origin (no cookies, storage or access to the editor) whose CSP allows only
10
+ // its own scripts and blob workers, so the code cannot reach the network,
11
+ // load fonts or images, or open frames. The worker inherits that policy.
12
+ const FRAME_CSP =
13
+ "default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' blob:; worker-src blob:"
14
+ const FRAME_HTML = `<!doctype html><meta http-equiv="Content-Security-Policy" content="${FRAME_CSP}"><script>
15
+ let worker
16
+ addEventListener('message', (event) => {
17
+ if (event.source !== parent) return
18
+ const data = event.data
19
+ if (data && data.type === 'start') {
20
+ worker = new Worker(URL.createObjectURL(new Blob([data.source], { type: 'text/javascript' })))
21
+ worker.onmessage = (e) => parent.postMessage(e.data, '*', e.data && e.data.output ? [e.data.output.glb] : [])
22
+ worker.onerror = (e) => parent.postMessage({ crashed: e.message || 'The compile worker crashed' }, '*')
23
+ return
24
+ }
25
+ if (worker) worker.postMessage(data)
26
+ })
27
+ parent.postMessage({ frame: true }, '*')
28
+ </script>`
29
+
30
+ type Pending = {
31
+ code: string
32
+ resolve: (output: GeometryScriptCompileOutput) => void
33
+ reject: (error: Error) => void
34
+ timeout: ReturnType<typeof setTimeout>
35
+ }
36
+
37
+ type FrameMessage =
38
+ | { frame: true }
39
+ | { ready: true }
40
+ | { crashed: string }
41
+ | GeometryScriptWorkerResponse
42
+
43
+ let frame: HTMLIFrameElement | null = null
44
+ let started: Promise<Window> | null = null
45
+ let nextId = 1
46
+ const pending = new Map<number, Pending>()
47
+ let onFrameMessage: ((message: FrameMessage) => void) | null = null
48
+
49
+ function reset(error: Error) {
50
+ frame?.remove()
51
+ frame = null
52
+ started = null
53
+ for (const entry of pending.values()) {
54
+ clearTimeout(entry.timeout)
55
+ entry.reject(error)
56
+ }
57
+ pending.clear()
58
+ }
59
+
60
+ if (typeof window !== 'undefined') {
61
+ window.addEventListener('message', (event: MessageEvent<FrameMessage>) => {
62
+ if (!frame || event.source !== frame.contentWindow) return
63
+ const message = event.data
64
+ if ('crashed' in message) return reset(new Error(message.crashed))
65
+ if ('id' in message) {
66
+ const entry = pending.get(message.id)
67
+ if (!entry) return
68
+ pending.delete(message.id)
69
+ clearTimeout(entry.timeout)
70
+ if (message.ok) withOwnHashes(message.output, entry.code).then(entry.resolve, entry.reject)
71
+ else entry.reject(new Error(message.error))
72
+ return
73
+ }
74
+ onFrameMessage?.(message)
75
+ })
76
+ }
77
+
78
+ async function sha256(bytes: ArrayBuffer | Uint8Array): Promise<string> {
79
+ const hash = await crypto.subtle.digest('SHA-256', bytes as BufferSource)
80
+ return Array.from(new Uint8Array(hash), (b) => b.toString(16).padStart(2, '0')).join('')
81
+ }
82
+
83
+ /** The artifact and script hashes and the language, set here: nothing the sandboxed code reports about itself is trusted. */
84
+ async function withOwnHashes(
85
+ output: GeometryScriptCompileOutput,
86
+ code: string,
87
+ ): Promise<GeometryScriptCompileOutput> {
88
+ const [artifact, script] = await Promise.all([
89
+ sha256(output.glb),
90
+ sha256(new TextEncoder().encode(code)),
91
+ ])
92
+ return { ...output, sha256: artifact, script, language: 'three' }
93
+ }
94
+
95
+ /** Starts the sandbox frame and its compile worker once; a crash or timeout starts a fresh one. */
96
+ function startSandbox(): Promise<Window> {
97
+ if (started) return started
98
+ started = (async () => {
99
+ if (typeof document === 'undefined') throw new Error('Geometry scripts compile in a browser')
100
+ const { sandboxWorkerSource } = await import('@pascal-app/geometry-script/sandbox')
101
+ const iframe = document.createElement('iframe')
102
+ iframe.sandbox.add('allow-scripts')
103
+ iframe.setAttribute('aria-hidden', 'true')
104
+ iframe.tabIndex = -1
105
+ iframe.style.display = 'none'
106
+ iframe.srcdoc = FRAME_HTML
107
+ frame = iframe
108
+ const ready = new Promise<Window>((resolve, reject) => {
109
+ const timer = setTimeout(
110
+ () => reject(new Error('The script sandbox did not start')),
111
+ START_TIMEOUT_MS,
112
+ )
113
+ onFrameMessage = (message) => {
114
+ const target = iframe.contentWindow
115
+ if (!target) return
116
+ if ('frame' in message)
117
+ target.postMessage({ type: 'start', source: sandboxWorkerSource }, '*')
118
+ if ('ready' in message) {
119
+ clearTimeout(timer)
120
+ onFrameMessage = null
121
+ resolve(target)
122
+ }
123
+ }
124
+ })
125
+ document.body.appendChild(iframe)
126
+ return ready
127
+ })()
128
+ started.catch((error: unknown) =>
129
+ reset(error instanceof Error ? error : new Error(String(error))),
130
+ )
131
+ return started
132
+ }
133
+
134
+ /**
135
+ * Runs a geometry script in the sandbox and returns its GLB + manifest. A
136
+ * script that hangs is stopped with its sandbox; the next compile starts fresh.
137
+ */
138
+ export async function compileGeometryScriptInWorker(input: {
139
+ code: string
140
+ params?: Record<string, GeometryScriptParamValue>
141
+ }): Promise<GeometryScriptCompileOutput> {
142
+ const target = await startSandbox()
143
+ const id = nextId++
144
+ const request: GeometryScriptWorkerRequest = { id, code: input.code, params: input.params }
145
+ return new Promise((resolve, reject) => {
146
+ const timeout = setTimeout(() => {
147
+ reset(new Error(`The script ran longer than ${COMPILE_TIMEOUT_MS / 1000} s and was stopped`))
148
+ }, COMPILE_TIMEOUT_MS)
149
+ pending.set(id, { code: input.code, resolve, reject, timeout })
150
+ target.postMessage(request, '*')
151
+ })
152
+ }
@@ -0,0 +1,12 @@
1
+ import type { GeometryScriptParamValue } from '@pascal-app/core'
2
+ import type { GeometryScriptCompileOutput } from '@pascal-app/geometry-script'
3
+
4
+ export type GeometryScriptWorkerRequest = {
5
+ id: number
6
+ code: string
7
+ params?: Record<string, GeometryScriptParamValue>
8
+ }
9
+
10
+ export type GeometryScriptWorkerResponse =
11
+ | { id: number; ok: true; output: GeometryScriptCompileOutput }
12
+ | { id: number; ok: false; error: string }