@pascal-app/editor 1.0.5 → 1.0.7

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 (317) hide show
  1. package/package.json +20 -17
  2. package/src/components/editor/action-menu-button.tsx +4 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
  4. package/src/components/editor/bake-thumbnail.tsx +3 -2
  5. package/src/components/editor/custom-camera-controls.tsx +20 -8
  6. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  7. package/src/components/editor/first-person-controls.tsx +72 -61
  8. package/src/components/editor/floating-action-menu.tsx +27 -9
  9. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  10. package/src/components/editor/floorplan-panel.test.ts +220 -0
  11. package/src/components/editor/floorplan-panel.tsx +285 -132
  12. package/src/components/editor/footprint-height-handle.tsx +1 -1
  13. package/src/components/editor/grid.tsx +24 -15
  14. package/src/components/editor/group-actions.ts +19 -1
  15. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  16. package/src/components/editor/group-move-3d.ts +27 -2
  17. package/src/components/editor/group-selection-box-3d.tsx +4 -1
  18. package/src/components/editor/group-transform-shared.ts +20 -0
  19. package/src/components/editor/index.tsx +130 -20
  20. package/src/components/editor/inspector-extension-actions.tsx +36 -0
  21. package/src/components/editor/localized-html.tsx +22 -0
  22. package/src/components/editor/node-action-menu.tsx +46 -12
  23. package/src/components/editor/node-arrow-handles.tsx +1 -2
  24. package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
  25. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  26. package/src/components/editor/registry-action-contributions.test.tsx +67 -6
  27. package/src/components/editor/registry-action-contributions.tsx +115 -15
  28. package/src/components/editor/riser-diagram-panel.tsx +59 -9
  29. package/src/components/editor/room-controls.tsx +1 -1
  30. package/src/components/editor/room-handles-2d.tsx +156 -6
  31. package/src/components/editor/room-handles.tsx +241 -51
  32. package/src/components/editor/room-highlight.tsx +2 -0
  33. package/src/components/editor/room-labels.tsx +29 -15
  34. package/src/components/editor/selection-manager.tsx +15 -102
  35. package/src/components/editor/separator-end-grip.tsx +74 -0
  36. package/src/components/editor/site-edge-labels.tsx +1 -1
  37. package/src/components/editor/thumbnail-generator.test.ts +15 -6
  38. package/src/components/editor/thumbnail-generator.tsx +26 -1
  39. package/src/components/editor/use-floorplan-background-placement.ts +55 -52
  40. package/src/components/editor/views/view-bar.tsx +111 -26
  41. package/src/components/editor/wall-measurement-label.tsx +1 -1
  42. package/src/components/editor/wall-move-side-handles.tsx +5 -1
  43. package/src/components/editor/wall-pull-grip.tsx +193 -0
  44. package/src/components/editor/wall-snap-beacon-layer.tsx +81 -1
  45. package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
  46. package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
  47. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
  48. package/src/components/editor-2d/floorplan-group-move.tsx +31 -3
  49. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
  50. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +22 -13
  51. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +11 -3
  52. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +7 -1
  53. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
  54. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
  55. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +12 -1
  56. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +197 -37
  57. package/src/components/procedural-items/inspector.tsx +59 -12
  58. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
  59. package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
  60. package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
  61. package/src/components/tools/distribution/distribution-run-tool.tsx +50 -26
  62. package/src/components/tools/item/placement-history.test.tsx +1 -1
  63. package/src/components/tools/item/placement-strategies.test.ts +64 -1
  64. package/src/components/tools/item/placement-strategies.ts +5 -9
  65. package/src/components/tools/item/use-placement-coordinator.tsx +30 -34
  66. package/src/components/tools/registry/item-surface-move.test.ts +11 -0
  67. package/src/components/tools/select/select-candidates.test.ts +45 -0
  68. package/src/components/tools/select/select-candidates.ts +13 -2
  69. package/src/components/tools/shared/cursor-sphere.tsx +10 -3
  70. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  71. package/src/components/tools/shared/placement-box.tsx +1 -1
  72. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  73. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  74. package/src/components/tools/wall/wall-drafting.test.ts +299 -23
  75. package/src/components/tools/wall/wall-drafting.ts +132 -24
  76. package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
  77. package/src/components/tools/wall/wall-pull-snap.ts +563 -0
  78. package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
  79. package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
  80. package/src/components/tools/zone/zone-tool.tsx +3 -3
  81. package/src/components/ui/action-menu/action-button.tsx +2 -0
  82. package/src/components/ui/action-menu/camera-actions.tsx +8 -7
  83. package/src/components/ui/action-menu/control-modes.tsx +9 -5
  84. package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
  85. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
  86. package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
  87. package/src/components/ui/action-menu/index.tsx +1 -0
  88. package/src/components/ui/action-menu/measurement-control.tsx +35 -29
  89. package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
  90. package/src/components/ui/action-menu/structure-tools.tsx +25 -23
  91. package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
  92. package/src/components/ui/action-menu/view-toggles.tsx +256 -51
  93. package/src/components/ui/build-panel/build-panel.tsx +163 -25
  94. package/src/components/ui/command-palette/editor-commands.tsx +38 -11
  95. package/src/components/ui/controls/finish-swatch.tsx +45 -19
  96. package/src/components/ui/controls/material-picker.tsx +24 -17
  97. package/src/components/ui/controls/metric-control.test.ts +52 -0
  98. package/src/components/ui/controls/metric-control.tsx +62 -24
  99. package/src/components/ui/controls/segmented-control.tsx +5 -2
  100. package/src/components/ui/controls/slider-control.test.tsx +23 -0
  101. package/src/components/ui/controls/slider-control.tsx +30 -14
  102. package/src/components/ui/floating-level-selector.tsx +100 -48
  103. package/src/components/ui/helpers/building-helper.tsx +5 -3
  104. package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
  105. package/src/components/ui/helpers/helper-manager.test.tsx +53 -3
  106. package/src/components/ui/helpers/helper-manager.tsx +82 -57
  107. package/src/components/ui/helpers/item-helper.tsx +6 -4
  108. package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
  109. package/src/components/ui/level-duplicate-dialog.tsx +32 -23
  110. package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
  111. package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
  112. package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
  113. package/src/components/ui/panels/floor-foundation-panel.tsx +113 -38
  114. package/src/components/ui/panels/floor-opening-panel.tsx +73 -25
  115. package/src/components/ui/panels/inspector-extensions.tsx +66 -0
  116. package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
  117. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -6
  118. package/src/components/ui/panels/multi-field-value.test.ts +27 -0
  119. package/src/components/ui/panels/multi-field-value.ts +20 -1
  120. package/src/components/ui/panels/multi-height-mode.tsx +8 -7
  121. package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
  122. package/src/components/ui/panels/multi-selection-panel.tsx +75 -35
  123. package/src/components/ui/panels/node-display.ts +35 -24
  124. package/src/components/ui/panels/panel-manager.tsx +5 -3
  125. package/src/components/ui/panels/panel-wrapper.tsx +22 -109
  126. package/src/components/ui/panels/parametric-field-control.tsx +13 -10
  127. package/src/components/ui/panels/parametric-inspector.tsx +18 -12
  128. package/src/components/ui/panels/reference-panel.tsx +51 -35
  129. package/src/components/ui/panels/room-construction-rows.tsx +464 -65
  130. package/src/components/ui/panels/room-name-options.test.ts +36 -3
  131. package/src/components/ui/panels/room-name-options.ts +110 -11
  132. package/src/components/ui/panels/room-openings-field.tsx +21 -12
  133. package/src/components/ui/panels/room-panel.tsx +206 -99
  134. package/src/components/ui/panels/shape-choice.tsx +7 -5
  135. package/src/components/ui/panels/wall-region-list.tsx +7 -7
  136. package/src/components/ui/primitives/shortcut-token.tsx +10 -6
  137. package/src/components/ui/sidebar/icon-rail.tsx +11 -9
  138. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
  139. package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
  140. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
  141. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
  142. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -2
  144. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
  145. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
  146. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
  147. package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
  148. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
  149. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
  150. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
  151. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
  152. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
  153. package/src/components/ui/sidebar/panels/site-panel/index.tsx +117 -64
  154. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
  155. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
  156. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
  157. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
  158. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
  159. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
  160. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +8 -3
  161. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
  162. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
  163. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
  164. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
  165. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
  166. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
  167. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
  168. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
  169. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
  170. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +18 -8
  171. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
  172. package/src/components/ui/sidebar/tab-bar.tsx +4 -1
  173. package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
  174. package/src/components/ui/snap-target-badge.tsx +7 -6
  175. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
  176. package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
  177. package/src/components/viewer/floorplan-compass-button.tsx +12 -3
  178. package/src/components/viewer/floorplan-preview.tsx +27 -19
  179. package/src/components/viewer/render-settings-menu.tsx +294 -0
  180. package/src/components/viewer/sky-control.tsx +710 -0
  181. package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
  182. package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
  183. package/src/components/viewer/viewer-controls-bar.tsx +278 -260
  184. package/src/components/viewer/viewer-hover-label.tsx +12 -4
  185. package/src/components/viewer/viewer-rooms.tsx +61 -4
  186. package/src/components/viewer/viewer-scene-header.tsx +28 -17
  187. package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
  188. package/src/components/viewer-overlay.tsx +8 -0
  189. package/src/components/walkthrough-hud.tsx +75 -2
  190. package/src/hooks/use-keyboard.test.ts +85 -4
  191. package/src/hooks/use-keyboard.ts +65 -7
  192. package/src/hooks/use-selected-room.ts +12 -1
  193. package/src/i18n/floor-footprint-name.ts +30 -0
  194. package/src/i18n/level-name.ts +39 -0
  195. package/src/i18n/locales/en/messages.po +15117 -0
  196. package/src/i18n/locales/en/messages.ts +1 -0
  197. package/src/i18n/locales/en-XA/messages.ts +1 -0
  198. package/src/i18n/locales/es/messages.po +37501 -0
  199. package/src/i18n/locales/es/messages.ts +1 -0
  200. package/src/i18n/locales/fr/messages.po +37501 -0
  201. package/src/i18n/locales/fr/messages.ts +1 -0
  202. package/src/i18n/locales/ja/messages.po +37501 -0
  203. package/src/i18n/locales/ja/messages.ts +1 -0
  204. package/src/i18n/locales/zh-CN/messages.po +37501 -0
  205. package/src/i18n/locales/zh-CN/messages.ts +1 -0
  206. package/src/i18n/locales.test.ts +63 -0
  207. package/src/i18n/locales.ts +32 -0
  208. package/src/i18n/message-slot.tsx +25 -0
  209. package/src/i18n/node-messages.ts +169 -0
  210. package/src/i18n/pseudo-locale.ts +17 -0
  211. package/src/i18n/published-entry.test.ts +17 -0
  212. package/src/i18n/scene-theme-name.ts +16 -0
  213. package/src/index.tsx +156 -4
  214. package/src/lib/add-storey.test.ts +3 -3
  215. package/src/lib/contextual-help.test.ts +20 -1
  216. package/src/lib/contextual-help.ts +95 -19
  217. package/src/lib/continuation.ts +38 -11
  218. package/src/lib/floor-footprints.test.ts +2 -1
  219. package/src/lib/floor-foundation-support.test.ts +7 -3
  220. package/src/lib/floorplan/dimension-drive.test.ts +36 -2
  221. package/src/lib/floorplan/dimension-drive.ts +68 -3
  222. package/src/lib/floorplan/floorplan-export.test.ts +16 -5
  223. package/src/lib/floorplan/floorplan-export.tsx +13 -16
  224. package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
  225. package/src/lib/floorplan/floorplan-extension.ts +37 -18
  226. package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
  227. package/src/lib/floorplan/floorplan-mode.ts +14 -12
  228. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
  229. package/src/lib/floorplan/floorplan-readonly.ts +3 -3
  230. package/src/lib/floorplan/schedules.ts +10 -6
  231. package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
  232. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
  233. package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
  234. package/src/lib/geometry-script/author.test.ts +114 -0
  235. package/src/lib/geometry-script/author.ts +55 -23
  236. package/src/lib/geometry-script/client.ts +3 -0
  237. package/src/lib/geometry-script/finish-scripts.ts +7 -7
  238. package/src/lib/glb-export.test.ts +327 -0
  239. package/src/lib/glb-export.ts +70 -2
  240. package/src/lib/grid-frame.test.ts +56 -3
  241. package/src/lib/grid-frame.ts +28 -1
  242. package/src/lib/history.test.ts +2 -1
  243. package/src/lib/hud-title.ts +48 -24
  244. package/src/lib/ifc-export.test.ts +41 -1
  245. package/src/lib/inspector-card-mode.test.ts +3 -31
  246. package/src/lib/inspector-card-mode.ts +5 -10
  247. package/src/lib/inspector-expanded.test.ts +13 -0
  248. package/src/lib/inspector-expanded.ts +13 -1
  249. package/src/lib/keyboard-pan.test.ts +7 -0
  250. package/src/lib/keyboard-pan.ts +9 -2
  251. package/src/lib/layer-thickness.test.ts +22 -0
  252. package/src/lib/layer-thickness.ts +79 -0
  253. package/src/lib/lot/drop-in.test.ts +59 -1
  254. package/src/lib/lot/drop-in.ts +42 -50
  255. package/src/lib/material-swatch-style.ts +24 -0
  256. package/src/lib/measurement-parser.ts +7 -3
  257. package/src/lib/measurements.test.ts +17 -0
  258. package/src/lib/measurements.ts +20 -5
  259. package/src/lib/mezzanine-selection.test.ts +3 -1
  260. package/src/lib/orbit-near-plane.test.ts +12 -0
  261. package/src/lib/orbit-near-plane.ts +13 -0
  262. package/src/lib/paint-regions.test.ts +8 -5
  263. package/src/lib/paint-scope.test.ts +53 -0
  264. package/src/lib/paint-scope.ts +18 -4
  265. package/src/lib/panel-rows.ts +17 -8
  266. package/src/lib/reference-scale.ts +83 -0
  267. package/src/lib/room-built-on.test.ts +26 -12
  268. package/src/lib/room-built-on.ts +20 -2
  269. package/src/lib/room-construction.test.ts +28 -1
  270. package/src/lib/room-construction.ts +14 -0
  271. package/src/lib/room-controls-phase6.test.ts +140 -0
  272. package/src/lib/room-handle-drag.ts +557 -42
  273. package/src/lib/room-name-catalog.test.ts +8 -0
  274. package/src/lib/room-name-catalog.ts +8 -1
  275. package/src/lib/room-name-messages.test.ts +132 -0
  276. package/src/lib/room-name-messages.ts +147 -0
  277. package/src/lib/room-name-storage.test.ts +334 -0
  278. package/src/lib/room-push-dimensions.test.ts +37 -0
  279. package/src/lib/room-push-dimensions.ts +226 -2
  280. package/src/lib/room-selection.test.ts +23 -6
  281. package/src/lib/scene-clipboard.test.ts +70 -49
  282. package/src/lib/scene-clipboard.ts +9 -4
  283. package/src/lib/selection-cycle.ts +63 -0
  284. package/src/lib/separator-edit.test.ts +431 -0
  285. package/src/lib/separator-edit.ts +179 -0
  286. package/src/lib/separator-lines.ts +38 -0
  287. package/src/lib/sfx-bus.ts +27 -1
  288. package/src/lib/sfx-player.ts +16 -0
  289. package/src/lib/tool-cursor-icon.ts +5 -1
  290. package/src/lib/tool-shortcuts.ts +130 -0
  291. package/src/lib/viewer-selection.test.ts +28 -24
  292. package/src/lib/viewer-selection.ts +20 -5
  293. package/src/lib/wall-draw-variant.test.ts +7 -6
  294. package/src/lib/wall-draw-variant.ts +107 -6
  295. package/src/lib/wall-nudge.test.ts +44 -0
  296. package/src/lib/wall-nudge.ts +131 -0
  297. package/src/lib/wall-parallel-snap.test.ts +83 -0
  298. package/src/lib/wall-parallel-snap.ts +176 -0
  299. package/src/lib/wall-placement-pointer.test.ts +23 -0
  300. package/src/lib/wall-placement-pointer.ts +18 -0
  301. package/src/lib/wall-pull.test.ts +245 -0
  302. package/src/lib/wall-pull.ts +209 -0
  303. package/src/lib/wall-push-line-up.test.ts +132 -0
  304. package/src/lib/wall-region-list.ts +14 -5
  305. package/src/lib/wall-run-snap.test.ts +188 -0
  306. package/src/lib/wall-run-snap.ts +83 -0
  307. package/src/lib/wall-type.test.ts +125 -0
  308. package/src/lib/wall-type.ts +119 -0
  309. package/src/lib/window-interaction.test.ts +42 -0
  310. package/src/lib/window-interaction.ts +47 -5
  311. package/src/lib/world-grid-snap.test.ts +139 -2
  312. package/src/lib/world-grid-snap.ts +102 -105
  313. package/src/store/use-alignment-guides.ts +27 -1
  314. package/src/store/use-editor.tsx +48 -0
  315. package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
  316. package/src/store/use-floorplan-annotation-visibility.ts +47 -22
  317. package/src/store/use-wall-snap-indicator.ts +15 -2
@@ -1,5 +1,5 @@
1
1
  import type { AnyNode, AnyNodeId, DoorNode, WallNode, WindowNode, ZoneNode } from '@pascal-app/core'
2
- import { deriveZoneQuantityReport } from '@pascal-app/core'
2
+ import { deriveZoneQuantityReport, resolveHostedOpeningFrameDepth } from '@pascal-app/core'
3
3
  import { collectFloorplanSchedules } from './floorplan-export'
4
4
  import type { FloorplanSchedule } from './floorplan-extension'
5
5
  import { resolveMarkDetail } from './marks'
@@ -150,7 +150,7 @@ function openingRow(
150
150
  ? `${formatScheduleLength(opening.roughOpeningWidth, unit)} x ${formatScheduleLength(opening.roughOpeningHeight, unit)}`
151
151
  : null,
152
152
  material: openingMaterial(opening),
153
- frame: openingFrame(opening, unit),
153
+ frame: openingFrame(opening, nodes, unit),
154
154
  hardware: opening.type === 'door' ? doorHardware(opening as DoorNode) : null,
155
155
  count: 1,
156
156
  remarks: remarks.join('; '),
@@ -190,10 +190,14 @@ function openingMaterial(opening: DoorNode | WindowNode): string | null {
190
190
  return null
191
191
  }
192
192
 
193
- function openingFrame(opening: DoorNode | WindowNode, unit: ScheduleUnit): string | null {
194
- const thickness = (opening as DoorNode).frameThickness
195
- const depth = (opening as DoorNode).frameDepth
196
- if (typeof thickness !== 'number' || typeof depth !== 'number') return null
193
+ function openingFrame(
194
+ opening: DoorNode | WindowNode,
195
+ nodes: Readonly<Record<string, AnyNode>>,
196
+ unit: ScheduleUnit,
197
+ ): string | null {
198
+ const thickness = opening.frameThickness
199
+ if (typeof thickness !== 'number') return null
200
+ const depth = resolveHostedOpeningFrameDepth(opening, nodes)
197
201
  return `${formatScheduleLength(thickness, unit)} / ${formatScheduleLength(depth, unit)}`
198
202
  }
199
203
 
@@ -1,5 +1,12 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { createTerrainField, type LevelNode, quantize, type SceneSnapshot } from '@pascal-app/core'
2
+ import {
3
+ createTerrainField,
4
+ LevelNode,
5
+ quantize,
6
+ type SceneSnapshot,
7
+ SlabNode,
8
+ ZoneNode,
9
+ } from '@pascal-app/core'
3
10
  import { buildSitePlanDrawing, setbacksWarning } from './build-site-plan-drawing'
4
11
  import { registerSitePlanContributor } from './contributors'
5
12
  import { computeSiteCoverage } from './coverage'
@@ -14,6 +21,7 @@ import {
14
21
  streetEdgeNames,
15
22
  UNNAMED_STREET,
16
23
  } from './site-annotations'
24
+ import { outdoorParts } from './site-parts'
17
25
 
18
26
  /** The QA lot (4121 NW 34th St, Gainesville): a through lot, NW 34th Terrace on the west edge (1), NW 34th Street on the east (3). */
19
27
  const LOT: [number, number][] = [
@@ -436,6 +444,43 @@ describe('the setbacks line a plans examiner reads', () => {
436
444
  })
437
445
 
438
446
  describe('the finish floor elevation', () => {
447
+ test('outdoor plates use their room type for site parts and stay out of the house floor elevation', () => {
448
+ const polygon: [number, number][] = [
449
+ [0, 0],
450
+ [2, 0],
451
+ [2, 2],
452
+ [0, 2],
453
+ ]
454
+ const room = ZoneNode.parse({ name: 'Deck', roomType: 'deck', polygon })
455
+ const plate = SlabNode.parse({
456
+ zoneIds: [room.id],
457
+ plateRole: 'sunken',
458
+ polygon,
459
+ elevation: 0.4,
460
+ })
461
+ const floor = SlabNode.parse({
462
+ plateRole: 'base',
463
+ zoneIds: [room.id],
464
+ polygon: [],
465
+ elevation: 0.05,
466
+ })
467
+ const level = LevelNode.parse({ children: [plate.id, floor.id, room.id] })
468
+ for (const metadata of [{}, { floor: 'porch-slab' }]) {
469
+ const scene = {
470
+ nodes: {
471
+ [level.id]: level,
472
+ [room.id]: room,
473
+ [plate.id]: { ...plate, metadata },
474
+ [floor.id]: floor,
475
+ },
476
+ } as unknown as SceneSnapshot
477
+ expect(outdoorParts(scene, level, null, [], [])).toMatchObject([
478
+ { id: plate.id, kind: 'deck', wood: true },
479
+ ])
480
+ expect(floorTops(scene, level)).toEqual({ floor: 0.05, garage: null })
481
+ }
482
+ })
483
+
439
484
  test('the driveway and walk (flatwork, no floor tag) never become the FF', () => {
440
485
  const nodes = {
441
486
  level_0: {
@@ -11,7 +11,9 @@ import {
11
11
  type AnyNodeId,
12
12
  type BuildingNode,
13
13
  type FloorplanGeometry,
14
+ floorPlateRoom,
14
15
  heightAt,
16
+ isOutdoorRoomType,
15
17
  type LevelNode,
16
18
  outwardNormal,
17
19
  type SceneSnapshot,
@@ -1072,6 +1074,7 @@ export function floorTops(
1072
1074
  if (n?.type !== 'slab') continue
1073
1075
  // the driveway and walks are paving at grade, never the finish floor
1074
1076
  if (flatworkKindOf(n)) continue
1077
+ if (isOutdoorRoomType(floorPlateRoom(n, (id) => scene.nodes[id])?.roomType)) continue
1075
1078
  const tag = n.metadata?.floor
1076
1079
  const top = typeof n.elevation === 'number' ? n.elevation : 0.05
1077
1080
  if (tag === 'garage-slab-at-grade') garage = garage === null ? top : Math.min(garage, top)
@@ -10,7 +10,9 @@ import {
10
10
  type AnyNode,
11
11
  type AnyNodeId,
12
12
  type BuildingNode,
13
+ floorPlateRoom,
13
14
  getRoofSegmentDeckReach,
15
+ isOutdoorRoomType,
14
16
  type LevelNode,
15
17
  migrateSiteMetadata,
16
18
  type RoofSegmentNode,
@@ -201,7 +203,7 @@ function coveredShare(ring: readonly Pt[], covers: readonly Pt[][]): number {
201
203
 
202
204
  /**
203
205
  * The level's outdoor slabs in site metres: porches, decks, patios and
204
- * landings (a generator `metadata.porch`, or a deck / porch-slab floor, or a
206
+ * landings (a generator `metadata.porch`, an outdoor room’s plate, or a
205
207
  * slab lying outside the house's walls) and the flatwork. The house's own
206
208
  * floors (the storey slab, a raised platform, the garage pad) and a porch
207
209
  * cover's beam are not outdoor parts. `roofs` decides which are covered.
@@ -247,7 +249,8 @@ export function outdoorParts(
247
249
  // a porch cover's beam box is not a floor
248
250
  if (/beam/i.test(floor) || /beam/i.test(slab.name ?? '')) continue
249
251
  const porch = meta.porch as { policy?: string; entrance?: string } | undefined
250
- const outdoorFloor = floor === 'deck' || floor === 'porch-slab'
252
+ const roomType = floorPlateRoom(child, (id) => scene.nodes[id])?.roomType
253
+ const outdoorFloor = isOutdoorRoomType(roomType)
251
254
  if (!porch && !outdoorFloor) {
252
255
  // a hand-drawn slab: the house's floor when it lies under the walls
253
256
  if (HOUSE_FLOORS.has(floor)) continue
@@ -257,7 +260,7 @@ export function outdoorParts(
257
260
  )
258
261
  if (walls.length === 0 || walls.some((w) => pointInPolygon(w, c[0], c[1]))) continue
259
262
  }
260
- const wood = floor === 'deck'
263
+ const wood = roomType === 'deck'
261
264
  const policy = porch?.policy ?? ''
262
265
  const kind: OutdoorKind =
263
266
  policy === 'deck' || (wood && policy !== 'full' && policy !== 'entry')
@@ -0,0 +1,114 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type ArtifactStore,
5
+ artifactUrl,
6
+ BuildingNode,
7
+ GeometryArtifactManifest,
8
+ generateId,
9
+ ItemNode,
10
+ LevelNode,
11
+ scriptSource,
12
+ } from '@pascal-app/core'
13
+ import {
14
+ captureRegistryVersions,
15
+ effectIds,
16
+ openDetachedScene,
17
+ SceneEnvironmentDisposedError,
18
+ type SceneSnapshot,
19
+ } from '@pascal-app/core/runtime'
20
+ import { compileAndStoreGeometryScript } from './author'
21
+
22
+ // A compile of an object for a detached scene reads the scene and mints the object's id before
23
+ // its first await: the scene records those reads and allocates that id in its synchronous scope,
24
+ // and a compile that resolves after the scene is disposed reaches nothing of it.
25
+
26
+ const sha = 'a'.repeat(64)
27
+ const compiled = {
28
+ sha256: sha,
29
+ script: 'b'.repeat(64),
30
+ language: 'three' as const,
31
+ mount: 'floor' as const,
32
+ params: {},
33
+ manifest: GeometryArtifactManifest.parse({
34
+ bounds: { min: [-0.5, 0, -0.5], max: [0.5, 1, 0.5] },
35
+ triangles: 12,
36
+ }),
37
+ }
38
+
39
+ function scene() {
40
+ const screen = ItemNode.parse({
41
+ id: 'item_screen',
42
+ parentId: 'level_0',
43
+ name: 'Screen',
44
+ source: scriptSource(compiled),
45
+ asset: {
46
+ id: 'script_screen',
47
+ category: 'screen',
48
+ name: 'Screen',
49
+ thumbnail: '',
50
+ source: 'mine',
51
+ src: artifactUrl(sha),
52
+ dimensions: [1, 1, 1],
53
+ },
54
+ })
55
+ const level = LevelNode.parse({ id: 'level_0', parentId: 'building_0', children: [screen.id] })
56
+ const building = BuildingNode.parse({ id: 'building_0', children: [level.id] })
57
+ const snapshot = {
58
+ nodes: Object.fromEntries([building, level, screen].map((node) => [node.id, node])),
59
+ rootNodeIds: [building.id],
60
+ collections: {},
61
+ materials: {},
62
+ installedPlugins: [],
63
+ } as unknown as SceneSnapshot
64
+ return openDetachedScene(snapshot, {
65
+ base: { kind: 'authority', seq: 1 },
66
+ context: {
67
+ activeLevelId: 'level_0',
68
+ selection: [],
69
+ units: 'metric',
70
+ catalogVersion: 'test',
71
+ registryVersions: captureRegistryVersions(),
72
+ },
73
+ ids: effectIds('effect_compile'),
74
+ })
75
+ }
76
+
77
+ const artifacts = {
78
+ url: () => null,
79
+ put: async (hash: string) => hash,
80
+ text: async () => 'export default function build() {}',
81
+ } as unknown as ArtifactStore
82
+
83
+ describe('compileAndStoreGeometryScript on a detached scene', () => {
84
+ test('reads the node it rebuilds before the first await', async () => {
85
+ const detached = scene()
86
+ const compiling = detached.run(() =>
87
+ compileAndStoreGeometryScript(
88
+ { nodeId: 'item_screen', params: {} },
89
+ { nodes: detached.reader().nodes as Record<string, AnyNode>, artifacts },
90
+ ),
91
+ )
92
+ expect(detached.dependencies()).toContainEqual(
93
+ expect.objectContaining({ kind: 'node', id: 'item_screen' }),
94
+ )
95
+ detached.dispose()
96
+ const error = await compiling.catch((thrown: unknown) => thrown)
97
+ expect(error).not.toBeInstanceOf(SceneEnvironmentDisposedError)
98
+ })
99
+
100
+ test("mints a new object's id in the scene's scope, before the first await", async () => {
101
+ const detached = scene()
102
+ const next = detached.run(() => {
103
+ void compileAndStoreGeometryScript(
104
+ { code: 'export default function build() {}', kind: 'item' },
105
+ { nodes: detached.reader().nodes as Record<string, AnyNode>, artifacts },
106
+ ).catch(() => {})
107
+ return generateId('item')
108
+ })
109
+ const ids = effectIds('effect_compile')
110
+ ids('item')
111
+ expect(next as string).toBe(ids('item'))
112
+ detached.dispose()
113
+ })
114
+ })
@@ -1,5 +1,7 @@
1
1
  import {
2
+ type AnyNode,
2
3
  type AnyNodeId,
4
+ type ArtifactStore,
3
5
  type CompiledGeometryScript,
4
6
  GEOMETRY_SCRIPT_MIME_TYPES,
5
7
  GeometryArtifactMetadata,
@@ -41,28 +43,46 @@ export function configureBlenderScriptBuilder(next: BlenderScriptBuilder | null)
41
43
  blenderBuilder = next
42
44
  }
43
45
 
46
+ /**
47
+ * What a compile reads from the scene and the artifact service it stores to. By default the
48
+ * editor's own; a detached scene passes its tracked nodes, so the reads are recorded there.
49
+ */
50
+ export type GeometryScriptScene = {
51
+ nodes?: Readonly<Record<string, AnyNode>>
52
+ artifacts?: ArtifactStore
53
+ }
54
+
44
55
  /**
45
56
  * The editor's compile step for `add_object`: runs the module (a three.js one
46
57
  * in the worker, a bpy one through the host's Blender builder) and stores the
47
58
  * GLB and the module text, so the core operation can reference both by hash.
48
59
  * Without `code`, the node's stored script is rebuilt; either way an edit keeps
49
60
  * the node's param values it does not override.
61
+ *
62
+ * Everything it reads from the scene, and the id of a new object, are taken
63
+ * synchronously, before the first await: a detached scene records reads and
64
+ * mints ids only inside its synchronous scope, and is disposed by the time a
65
+ * late compile resolves.
50
66
  */
51
- export async function compileAndStoreGeometryScript(input: {
52
- code?: string
53
- language?: GeometryScriptLanguage
54
- nodeId?: string
55
- params?: Record<string, GeometryScriptParamValue>
56
- kind?: 'item' | 'door' | 'window' | 'column'
57
- inputs?: Record<string, unknown>
58
- name?: string
59
- description?: string
60
- category?: string
61
- tags?: string[]
62
- provenance?: GeometryArtifactMetadata
63
- }): Promise<CompiledGeometryScript> {
64
- const code = input.code ?? (await storedScript(input.nodeId))
65
- const node = input.nodeId ? useScene.getState().nodes[input.nodeId as AnyNodeId] : undefined
67
+ export async function compileAndStoreGeometryScript(
68
+ input: {
69
+ code?: string
70
+ language?: GeometryScriptLanguage
71
+ nodeId?: string
72
+ params?: Record<string, GeometryScriptParamValue>
73
+ kind?: 'item' | 'door' | 'window' | 'column'
74
+ inputs?: Record<string, unknown>
75
+ name?: string
76
+ description?: string
77
+ category?: string
78
+ tags?: string[]
79
+ provenance?: GeometryArtifactMetadata
80
+ },
81
+ scene: GeometryScriptScene = {},
82
+ ): Promise<CompiledGeometryScript> {
83
+ const nodes = scene.nodes ?? useScene.getState().nodes
84
+ const artifacts = scene.artifacts ?? getArtifactStore()
85
+ const node = input.nodeId ? nodes[input.nodeId as AnyNodeId] : undefined
66
86
  const language = editedScriptLanguage(node, input)
67
87
  const params = editedScriptParams(node, input.params)
68
88
  const kind =
@@ -71,12 +91,16 @@ export async function compileAndStoreGeometryScript(input: {
71
91
  ? node.type
72
92
  : 'item')
73
93
  const nodeId = input.nodeId ?? generateId(kind)
94
+ const authored = isScriptedNode(node) ? scriptedObjectMeta(node) : {}
95
+ const script = input.code === undefined ? authoredScript(nodes, input.nodeId) : null
74
96
  const metadata = GeometryArtifactMetadata.parse({
75
- ...(isScriptedNode(node) ? scriptedObjectMeta(node) : {}),
97
+ ...authored,
76
98
  ...input.provenance,
77
99
  ...GeometryReuseFields.parse(input),
78
100
  kind,
79
101
  })
102
+
103
+ const code = input.code ?? (await readScript(artifacts, script!, input.nodeId!))
80
104
  if (language === 'bpy') {
81
105
  if (!blenderBuilder) throw new Error('This editor cannot run Blender (bpy) scripts')
82
106
  return { ...(await blenderBuilder({ code, params, nodeId, metadata })), nodeId }
@@ -87,10 +111,9 @@ export async function compileAndStoreGeometryScript(input: {
87
111
  inputs: input.inputs,
88
112
  })
89
113
  const context = { nodeId, metadata: { ...metadata, mount: compiled.mount } }
90
- const store = getArtifactStore()
91
114
  const [sha256] = await Promise.all([
92
- store.put(compiled.sha256, glb, 'model/gltf-binary', context),
93
- store.put(
115
+ artifacts.put(compiled.sha256, glb, 'model/gltf-binary', context),
116
+ artifacts.put(
94
117
  compiled.script,
95
118
  new TextEncoder().encode(code),
96
119
  GEOMETRY_SCRIPT_MIME_TYPES.three,
@@ -100,15 +123,24 @@ export async function compileAndStoreGeometryScript(input: {
100
123
  return { ...compiled, sha256, nodeId }
101
124
  }
102
125
 
103
- /** The module text of an authored object, read back from the artifact store. */
104
- export async function storedScript(nodeId: string | undefined): Promise<string> {
126
+ /** The hash of an authored object's script, read from the scene. */
127
+ function authoredScript(nodes: Readonly<Record<string, AnyNode>>, nodeId: string | undefined) {
105
128
  if (!nodeId) throw new Error('Pass the code to build a new object')
106
- const node = authoredObject(useScene.getState().nodes, nodeId)
107
- const code = await getArtifactStore().text(node.source.script)
129
+ return authoredObject(nodes as Record<string, AnyNode>, nodeId).source.script
130
+ }
131
+
132
+ async function readScript(artifacts: ArtifactStore, script: string, nodeId: string) {
133
+ const code = await artifacts.text(script)
108
134
  if (code === null) throw new Error(`The script of ${nodeId} could not be read`)
109
135
  return code
110
136
  }
111
137
 
138
+ /** The module text of an authored object, read back from the artifact store. */
139
+ export async function storedScript(nodeId: string | undefined): Promise<string> {
140
+ const script = authoredScript(useScene.getState().nodes, nodeId)
141
+ return readScript(getArtifactStore(), script, nodeId!)
142
+ }
143
+
112
144
  const rebuildGeneration = new Map<string, number>()
113
145
 
114
146
  /**
@@ -1,4 +1,5 @@
1
1
  import type { GeometryScriptParamValue } from '@pascal-app/core'
2
+ import { sdfChecksOn } from '@pascal-app/core/usage-field'
2
3
  import type { GeometryScriptCompileOutput } from '@pascal-app/geometry-script'
3
4
  import type { GeometryScriptWorkerRequest, GeometryScriptWorkerResponse } from './protocol'
4
5
 
@@ -148,6 +149,8 @@ export async function compileGeometryScriptInWorker(input: {
148
149
  code: input.code,
149
150
  params: input.params,
150
151
  inputs: input.inputs,
152
+ // the sandbox is its own context: the flag travels with the request, only when it is on
153
+ ...(sdfChecksOn() ? { usageShape: true } : {}),
151
154
  }
152
155
  return new Promise((resolve, reject) => {
153
156
  const timeout = setTimeout(() => {
@@ -17,9 +17,9 @@ import {
17
17
  import { compileGeometryScriptInWorker } from './client'
18
18
 
19
19
  /**
20
- * Runs a finish script (a room's floor, a wall's face) in the sandbox for what it finishes and
21
- * stores the GLB and the module text, answering the compile with the stored hashes
22
- * (`setRoomFloorScript` and `setWallFaceScript` take it with its `inputs`).
20
+ * Runs a finish script (a room's floor, a slab's surface, a wall's face) in the sandbox for what it
21
+ * finishes and stores the GLB and the module text, answering the compile with the stored hashes
22
+ * (`setRoomFloorScript`, `setSlabSurfaceScript` and `setWallFaceScript` take it with its `inputs`).
23
23
  */
24
24
  export async function compileAndStoreFinishScript(
25
25
  input: FinishScriptTarget & {
@@ -65,10 +65,10 @@ const SETTLE_MS = 150
65
65
 
66
66
  /**
67
67
  * Keeps every finish script built for what it finishes: after a scene change settles, each script
68
- * whose room's floor the structure re-derived, or whose wall face changed, runs again in the
69
- * sandbox. The new build is written outside history, as the reconcile's derived writes are, and
70
- * only while its floor or face is still the one it was built for; undo brings back the room or
71
- * wall and the script built for it together.
68
+ * whose room's floor the structure re-derived, whose slab was reshaped, or whose wall face changed,
69
+ * runs again in the sandbox. The new build is written outside history, as the reconcile's derived
70
+ * writes are, and only while its floor or face is still the one it was built for; undo brings back
71
+ * the room, slab or wall and the script built for it together.
72
72
  */
73
73
  export function initFinishScriptRefresh(): () => void {
74
74
  let timer: ReturnType<typeof setTimeout> | null = null