@pascal-app/editor 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/README.md +106 -0
  2. package/package.json +39 -37
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/bake-thumbnail.tsx +6 -15
  5. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  6. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  7. package/src/components/editor/custom-camera-controls.tsx +81 -8
  8. package/src/components/editor/editor-layout-mobile.tsx +1 -1
  9. package/src/components/editor/editor-layout-v2.tsx +1 -1
  10. package/src/components/editor/export-manager.tsx +14 -19
  11. package/src/components/editor/first-person/build-collider-world.test.ts +269 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +22 -5
  13. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  14. package/src/components/editor/first-person/ground-holes.ts +130 -0
  15. package/src/components/editor/first-person/mechanism-targets.test.ts +89 -2
  16. package/src/components/editor/first-person/mechanism-targets.ts +52 -0
  17. package/src/components/editor/first-person-controls.tsx +19 -11
  18. package/src/components/editor/floor-aware-camera-controls.test.ts +24 -0
  19. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  20. package/src/components/editor/floorplan-panel.tsx +72 -23
  21. package/src/components/editor/follow-pascal.test.tsx +912 -0
  22. package/src/components/editor/follow-pascal.tsx +354 -0
  23. package/src/components/editor/group-actions.ts +2 -0
  24. package/src/components/editor/group-move-3d.ts +2 -0
  25. package/src/components/editor/group-rotate-handle.tsx +2 -0
  26. package/src/components/editor/group-transform-shared.test.ts +460 -0
  27. package/src/components/editor/index.tsx +166 -79
  28. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  29. package/src/components/editor/room-highlight.tsx +13 -5
  30. package/src/components/editor/room-labels.test.ts +621 -128
  31. package/src/components/editor/room-labels.tsx +1090 -190
  32. package/src/components/editor/selection-manager.tsx +38 -117
  33. package/src/components/editor/slab-hole-highlights.tsx +2 -2
  34. package/src/components/editor/snapshot-capture-overlay.tsx +3 -2
  35. package/src/components/editor/snapshot-capture.test.ts +69 -58
  36. package/src/components/editor/snapshot-capture.ts +7 -1
  37. package/src/components/editor/thumbnail-generator.test.ts +350 -4
  38. package/src/components/editor/thumbnail-generator.tsx +128 -54
  39. package/src/components/editor/use-floorplan-background-placement.ts +3 -1
  40. package/src/components/editor/views/plugins-view.tsx +1 -8
  41. package/src/components/editor/views/view-bar.test.tsx +54 -0
  42. package/src/components/editor/views/view-bar.tsx +140 -17
  43. package/src/components/editor/wall-snap-beacon-layer.tsx +5 -4
  44. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  45. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  46. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +3 -4
  47. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +1 -1
  48. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +29 -7
  49. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  50. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  51. package/src/components/point-ask/bubble.tsx +538 -0
  52. package/src/components/point-ask/hover-chip.tsx +91 -0
  53. package/src/components/point-ask/mode-pill.tsx +92 -0
  54. package/src/components/point-ask/optional-motion.ts +22 -0
  55. package/src/components/point-ask/outline-corners.tsx +93 -0
  56. package/src/components/point-ask/pins.tsx +711 -0
  57. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  58. package/src/components/point-ask/point-ask-styles.ts +105 -0
  59. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  60. package/src/components/point-ask/region-marquee.tsx +64 -0
  61. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  62. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  63. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  64. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  65. package/src/components/tools/distribution/distribution-public.ts +9 -0
  66. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  67. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  68. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  69. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  70. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  71. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  72. package/src/components/tools/distribution/ports.test.ts +56 -0
  73. package/src/components/tools/distribution/ports.ts +378 -0
  74. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  75. package/src/components/tools/distribution/run-cursor.ts +36 -0
  76. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  77. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  78. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  79. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  80. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  81. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  82. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  83. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  84. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  85. package/src/components/tools/item/placement-history.test.tsx +38 -0
  86. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  87. package/src/components/tools/item/placement-strategies.ts +3 -6
  88. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  89. package/src/components/tools/item/use-draft-node.test.tsx +94 -94
  90. package/src/components/tools/item/use-placement-coordinator.tsx +55 -49
  91. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  92. package/src/components/tools/registry/item-surface-move.ts +2 -0
  93. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  94. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +63 -29
  96. package/src/components/tools/tool-manager.tsx +14 -5
  97. package/src/components/tools/wall/wall-drafting.test.ts +170 -57
  98. package/src/components/ui/action-menu/action-button.tsx +6 -1
  99. package/src/components/ui/action-menu/camera-actions.tsx +7 -3
  100. package/src/components/ui/action-menu/control-modes.tsx +13 -7
  101. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  102. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  103. package/src/components/ui/action-menu/index.tsx +56 -33
  104. package/src/components/ui/action-menu/measurement-control.tsx +11 -9
  105. package/src/components/ui/action-menu/placement.ts +20 -0
  106. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  107. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  108. package/src/components/ui/build-panel/build-panel.tsx +10 -3
  109. package/src/components/ui/command-palette/editor-commands.tsx +3 -12
  110. package/src/components/ui/command-palette/index.tsx +45 -15
  111. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  112. package/src/components/ui/controls/action-button.tsx +5 -2
  113. package/src/components/ui/controls/metric-control.tsx +1 -1
  114. package/src/components/ui/controls/segmented-control.tsx +21 -8
  115. package/src/components/ui/controls/select-control.tsx +1 -1
  116. package/src/components/ui/controls/slider-control.tsx +1 -1
  117. package/src/components/ui/controls/toggle-control.tsx +3 -3
  118. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  119. package/src/components/ui/floating-level-selector.tsx +150 -45
  120. package/src/components/ui/helpers/contextual-helper-panel.tsx +32 -7
  121. package/src/components/ui/helpers/helper-manager.test.tsx +54 -2
  122. package/src/components/ui/helpers/helper-manager.tsx +7 -1
  123. package/src/components/ui/helpers/item-helper.tsx +9 -1
  124. package/src/components/ui/panels/collections/collections-popover.tsx +8 -8
  125. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  126. package/src/components/ui/panels/mobile-selection-bar.tsx +6 -3
  127. package/src/components/ui/panels/multi-selection-panel.tsx +120 -3
  128. package/src/components/ui/panels/panel-wrapper.tsx +53 -27
  129. package/src/components/ui/panels/parametric-field-control.tsx +2 -2
  130. package/src/components/ui/panels/parametric-inspector.tsx +3 -3
  131. package/src/components/ui/panels/room-panel.tsx +65 -0
  132. package/src/components/ui/primitives/badge.tsx +8 -0
  133. package/src/components/ui/primitives/sidebar.tsx +1 -1
  134. package/src/components/ui/right-stack.tsx +21 -2
  135. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  136. package/src/components/ui/sidebar/icon-rail.tsx +3 -3
  137. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  138. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  139. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  140. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  141. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  142. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  143. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +107 -23
  144. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  145. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  146. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  147. package/src/components/ui/sidebar/panels/site-panel/index.tsx +5 -12
  148. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -10
  149. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -2
  150. package/src/components/ui/sidebar/tab-bar.tsx +4 -4
  151. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  152. package/src/components/viewer/viewer-rooms.tsx +16 -0
  153. package/src/hooks/use-auto-save.integration.test.tsx +96 -38
  154. package/src/hooks/use-auto-save.ts +26 -1
  155. package/src/hooks/use-ceiling-events.test.ts +123 -118
  156. package/src/hooks/use-grid-events.test.ts +105 -105
  157. package/src/hooks/use-keyboard.test.ts +65 -16
  158. package/src/hooks/use-keyboard.ts +29 -12
  159. package/src/hooks/use-placement-notice.test.ts +24 -1
  160. package/src/hooks/use-placement-notice.ts +37 -6
  161. package/src/index.tsx +71 -24
  162. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  163. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  164. package/src/lib/add-storey.test.ts +177 -0
  165. package/src/lib/add-storey.ts +59 -0
  166. package/src/lib/below-grade.test.tsx +171 -0
  167. package/src/lib/below-grade.ts +133 -0
  168. package/src/lib/camera-facing.test.ts +24 -0
  169. package/src/lib/camera-facing.ts +24 -0
  170. package/src/lib/direct-manipulation.ts +2 -0
  171. package/src/lib/download-project-file.ts +17 -0
  172. package/src/lib/editor-level-display.ts +9 -2
  173. package/src/lib/editor-views.ts +35 -1
  174. package/src/lib/export-texture-utils.ts +2 -4
  175. package/src/lib/floor-foundation-support.test.ts +1 -0
  176. package/src/lib/floorplan/floorplan-pdfkit-document.ts +31 -16
  177. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +39 -0
  178. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +4 -0
  179. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  180. package/src/lib/floorplan/sheet-export.test.ts +24 -0
  181. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +34 -45
  182. package/src/lib/floorplan/site-plan/coverage.ts +2 -4
  183. package/src/lib/floorplan/site-plan/front-edge.ts +2 -1
  184. package/src/lib/floorplan/site-plan/geometry.test.ts +9 -15
  185. package/src/lib/floorplan/site-plan/geometry.ts +1 -416
  186. package/src/lib/floorplan/site-plan/index.ts +0 -16
  187. package/src/lib/floorplan/site-plan/site-annotations.test.ts +117 -0
  188. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -2
  189. package/src/lib/floorplan/site-plan/site-parts.ts +14 -106
  190. package/src/lib/floorplan/site-plan/yards.test.ts +2 -1
  191. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  192. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  193. package/src/lib/follow-pascal-exterior.ts +39 -0
  194. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  195. package/src/lib/follow-pascal-presentation.ts +129 -0
  196. package/src/lib/follow-pascal-sight.ts +84 -0
  197. package/src/lib/follow-pascal-targets.test.ts +406 -0
  198. package/src/lib/follow-pascal-targets.ts +198 -0
  199. package/src/lib/follow-pascal.test.ts +350 -0
  200. package/src/lib/follow-pascal.ts +284 -0
  201. package/src/lib/fresh-planar-placement.test.ts +112 -0
  202. package/src/lib/fresh-planar-placement.ts +57 -3
  203. package/src/lib/geometry-script/author.ts +6 -1
  204. package/src/lib/geometry-script/client.ts +8 -1
  205. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  206. package/src/lib/geometry-script/protocol.ts +1 -0
  207. package/src/lib/glb-export.test.ts +187 -162
  208. package/src/lib/glb-export.ts +4 -1
  209. package/src/lib/history.test.ts +12 -7
  210. package/src/lib/interaction/scope.ts +4 -0
  211. package/src/lib/level-selection.test.ts +57 -0
  212. package/src/lib/level-selection.ts +16 -1
  213. package/src/lib/lot/clean-ring.test.ts +2 -1
  214. package/src/lib/lot/drop-in.ts +9 -24
  215. package/src/lib/lot/lot-patch.test.ts +1 -1
  216. package/src/lib/lot/lot-patch.ts +2 -2
  217. package/src/lib/measurement-kind.test.ts +6 -8
  218. package/src/lib/measurements.test.ts +23 -0
  219. package/src/lib/orbit-floor-clearance.test.ts +36 -0
  220. package/src/lib/orbit-floor-clearance.ts +48 -0
  221. package/src/lib/paint-pass-through.test.ts +11 -0
  222. package/src/lib/paint-pass-through.ts +4 -2
  223. package/src/lib/paint-scope.ts +1 -0
  224. package/src/lib/parametric-node-update.ts +16 -7
  225. package/src/lib/plugin-chat.ts +15 -0
  226. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  227. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  228. package/src/lib/point-ask/ask-created.test.ts +195 -0
  229. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  230. package/src/lib/point-ask/build-context.test.ts +173 -0
  231. package/src/lib/point-ask/build-context.ts +94 -0
  232. package/src/lib/point-ask/capture-crop.ts +129 -0
  233. package/src/lib/point-ask/choreography.ts +69 -0
  234. package/src/lib/point-ask/compare.test.ts +145 -0
  235. package/src/lib/point-ask/compare.ts +73 -0
  236. package/src/lib/point-ask/describe-target.test.ts +165 -0
  237. package/src/lib/point-ask/describe-target.ts +234 -0
  238. package/src/lib/point-ask/dictation.ts +79 -0
  239. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  240. package/src/lib/point-ask/edge-marker.ts +27 -0
  241. package/src/lib/point-ask/frame-loop.ts +25 -0
  242. package/src/lib/point-ask/history-marks.test.ts +94 -0
  243. package/src/lib/point-ask/history-marks.ts +39 -0
  244. package/src/lib/point-ask/host-api.ts +32 -0
  245. package/src/lib/point-ask/outline-box.test.ts +30 -0
  246. package/src/lib/point-ask/outline-box.ts +11 -0
  247. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  248. package/src/lib/point-ask/pick.test.ts +220 -0
  249. package/src/lib/point-ask/pick.ts +121 -0
  250. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  251. package/src/lib/point-ask/pin-machine.ts +217 -0
  252. package/src/lib/point-ask/placement.test.ts +203 -0
  253. package/src/lib/point-ask/placement.ts +141 -0
  254. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  255. package/src/lib/point-ask/projector.ts +83 -0
  256. package/src/lib/point-ask/region.test.ts +157 -0
  257. package/src/lib/point-ask/region.ts +136 -0
  258. package/src/lib/point-ask/retry.test.ts +217 -0
  259. package/src/lib/point-ask/retry.ts +37 -0
  260. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  261. package/src/lib/point-ask/scene-commit.ts +32 -0
  262. package/src/lib/point-ask/session.ts +1391 -0
  263. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  264. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  265. package/src/lib/point-ask/spring.test.ts +63 -0
  266. package/src/lib/point-ask/spring.ts +107 -0
  267. package/src/lib/point-ask/suggestions.test.ts +80 -0
  268. package/src/lib/point-ask/suggestions.ts +125 -0
  269. package/src/lib/point-ask/target-scope.test.ts +622 -0
  270. package/src/lib/point-ask/target-scope.ts +338 -0
  271. package/src/lib/point-ask/types.ts +48 -0
  272. package/src/lib/portable-export.test.ts +34 -1
  273. package/src/lib/portable-export.ts +119 -53
  274. package/src/lib/print-feature-thickness.ts +10 -1
  275. package/src/lib/print-roof-solids.test.ts +39 -1
  276. package/src/lib/print-roof-solids.ts +43 -10
  277. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  278. package/src/lib/room-assembly-overlay.ts +2 -3
  279. package/src/lib/room-selection.test.ts +45 -1
  280. package/src/lib/scene-clipboard.test.ts +246 -1
  281. package/src/lib/scene-clipboard.ts +44 -14
  282. package/src/lib/selection-routing.ts +1 -1
  283. package/src/lib/sfx-bus.ts +13 -0
  284. package/src/lib/sfx-player.test.ts +70 -9
  285. package/src/lib/sfx-player.ts +6 -0
  286. package/src/lib/stair-levels.test.ts +25 -0
  287. package/src/lib/stair-levels.ts +22 -3
  288. package/src/lib/terrace-draft.ts +2 -1
  289. package/src/lib/viewer-selection.ts +22 -11
  290. package/src/lib/wall-placement-pointer.test.ts +42 -0
  291. package/src/lib/wall-placement-pointer.ts +57 -0
  292. package/src/lib/window-changes.ts +35 -0
  293. package/src/store/use-audio.test.ts +146 -0
  294. package/src/store/use-audio.tsx +127 -3
  295. package/src/store/use-construction-reveal.test.ts +173 -0
  296. package/src/store/use-construction-reveal.ts +145 -0
  297. package/src/store/use-corner-snap-hint.test.ts +24 -0
  298. package/src/store/use-corner-snap-hint.ts +47 -0
  299. package/src/store/use-follow-pascal.test.ts +62 -0
  300. package/src/store/use-follow-pascal.ts +53 -0
  301. package/src/store/use-point-ask.ts +247 -0
  302. package/src/store/use-stair-build-preview.ts +25 -5
  303. package/src/store/view-layout.test.ts +27 -1
  304. package/src/store/view-layout.ts +4 -1
  305. package/src/test-utils/render-current-markup.tsx +16 -0
  306. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  307. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  308. package/src/components/tools/stair/stair-defaults.ts +0 -15
  309. package/src/components/tools/stair/stair-tool.tsx +0 -682
@@ -0,0 +1,127 @@
1
+ import {
2
+ type AnyNodeId,
3
+ type CompiledGeometryScript,
4
+ type FinishScriptHost,
5
+ type FinishScriptInputs,
6
+ type FinishScriptTarget,
7
+ finishScriptWrites,
8
+ GEOMETRY_SCRIPT_MIME_TYPES,
9
+ type GeometryScriptParamValue,
10
+ getArtifactStore,
11
+ pauseSceneHistory,
12
+ refreshFinishScripts,
13
+ resumeSceneHistory,
14
+ staleFinishScripts,
15
+ useScene,
16
+ } from '@pascal-app/core'
17
+ import { compileGeometryScriptInWorker } from './client'
18
+
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`).
23
+ */
24
+ export async function compileAndStoreFinishScript(
25
+ input: FinishScriptTarget & {
26
+ code: string
27
+ params?: Record<string, GeometryScriptParamValue>
28
+ inputs: FinishScriptInputs
29
+ },
30
+ ): Promise<CompiledGeometryScript> {
31
+ const { glb, ...compiled } = await compileGeometryScriptInWorker({
32
+ code: input.code,
33
+ params: input.params,
34
+ inputs: input.inputs,
35
+ })
36
+ const context = {
37
+ nodeId: input.nodeId,
38
+ metadata: { name: input.face ? 'Wall finish' : 'Floor finish', mount: compiled.mount },
39
+ }
40
+ const store = getArtifactStore()
41
+ const [sha256] = await Promise.all([
42
+ store.put(compiled.sha256, glb, 'model/gltf-binary', context),
43
+ store.put(
44
+ compiled.script,
45
+ new TextEncoder().encode(input.code),
46
+ GEOMETRY_SCRIPT_MIME_TYPES.three,
47
+ context,
48
+ ),
49
+ ])
50
+ return { ...compiled, sha256 }
51
+ }
52
+
53
+ /** The editor tab's host for finish scripts: the sandbox compile and the scene's artifact store. */
54
+ export const editorFinishScriptHost: FinishScriptHost = {
55
+ readScript: (sha256) => getArtifactStore().text(sha256),
56
+ build: compileAndStoreFinishScript,
57
+ }
58
+
59
+ /** The module text of a finish script, read back from the artifact store. */
60
+ export function finishScriptCode(sha256: string): Promise<string | null> {
61
+ return getArtifactStore().text(sha256)
62
+ }
63
+
64
+ const SETTLE_MS = 150
65
+
66
+ /**
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.
72
+ */
73
+ export function initFinishScriptRefresh(): () => void {
74
+ let timer: ReturnType<typeof setTimeout> | null = null
75
+ let running = false
76
+ let again = false
77
+
78
+ const run = async () => {
79
+ if (running) {
80
+ again = true
81
+ return
82
+ }
83
+ running = true
84
+ try {
85
+ const nodes = useScene.getState().nodes
86
+ if (useScene.getState().readOnly || staleFinishScripts(nodes).length === 0) return
87
+ const refresh = await refreshFinishScripts(nodes, editorFinishScriptHost)
88
+ for (const { nodeId, face, error } of refresh.failed)
89
+ console.error(
90
+ `[finish script] ${nodeId}${face ? ` face ${face}` : ''} could not be rebuilt: ${error}`,
91
+ )
92
+ const writes = finishScriptWrites(useScene.getState().nodes, refresh.rebuilt)
93
+ if (writes.length === 0) return
94
+ pauseSceneHistory(useScene)
95
+ try {
96
+ useScene
97
+ .getState()
98
+ .updateNodes(writes.map(({ id, data }) => ({ id: id as AnyNodeId, data })))
99
+ } finally {
100
+ resumeSceneHistory(useScene)
101
+ }
102
+ } finally {
103
+ running = false
104
+ if (again) {
105
+ again = false
106
+ schedule()
107
+ }
108
+ }
109
+ }
110
+
111
+ const schedule = () => {
112
+ if (timer) clearTimeout(timer)
113
+ timer = setTimeout(() => {
114
+ timer = null
115
+ void run()
116
+ }, SETTLE_MS)
117
+ }
118
+
119
+ const unsubscribe = useScene.subscribe((state, previous) => {
120
+ if (state.nodes !== previous.nodes) schedule()
121
+ })
122
+ schedule()
123
+ return () => {
124
+ unsubscribe()
125
+ if (timer) clearTimeout(timer)
126
+ }
127
+ }
@@ -5,6 +5,7 @@ export type GeometryScriptWorkerRequest = {
5
5
  id: number
6
6
  code: string
7
7
  params?: Record<string, GeometryScriptParamValue>
8
+ inputs?: Record<string, unknown>
8
9
  }
9
10
 
10
11
  export type GeometryScriptWorkerResponse =
@@ -95,21 +95,21 @@ function sceneWithVisibleAndHiddenBoxes(): {
95
95
  }
96
96
 
97
97
  describe('prepareSceneForExport', () => {
98
- test.each([
99
- true,
100
- false,
101
- ])('omits registered draft geometry absent from the saved document (onlyVisible=%s)', (onlyVisible) => {
102
- const { root, nodes } = sceneWithVisibleAndHiddenBoxes()
103
- const draft = sceneRegistry.nodes.get('item_hidden')!
104
- draft.name = 'Unplaced preview'
105
- nodes.item_hidden!.visible = true
106
- delete nodes.item_hidden
107
-
108
- const result = prepareSceneForExport(root, nodes, { onlyVisible })
109
- expect(result.scene.getObjectByName('Unplaced preview')).toBeUndefined()
110
- expect(result.scene.children).toHaveLength(1)
111
- expect(root.getObjectByName('Unplaced preview')).toBe(draft)
112
- })
98
+ test.each([true, false])(
99
+ 'omits registered draft geometry absent from the saved document (onlyVisible=%s)',
100
+ (onlyVisible) => {
101
+ const { root, nodes } = sceneWithVisibleAndHiddenBoxes()
102
+ const draft = sceneRegistry.nodes.get('item_hidden')!
103
+ draft.name = 'Unplaced preview'
104
+ nodes.item_hidden!.visible = true
105
+ delete nodes.item_hidden
106
+
107
+ const result = prepareSceneForExport(root, nodes, { onlyVisible })
108
+ expect(result.scene.getObjectByName('Unplaced preview')).toBeUndefined()
109
+ expect(result.scene.children).toHaveLength(1)
110
+ expect(root.getObjectByName('Unplaced preview')).toBe(draft)
111
+ },
112
+ )
113
113
 
114
114
  test('skips a refused subtree and reports its warning while exporting other objects', () => {
115
115
  const root = new THREE.Group()
@@ -457,134 +457,133 @@ describe('prepareSceneForExport', () => {
457
457
  }
458
458
  })
459
459
 
460
- test.each([
461
- 'sync',
462
- 'portable-sync',
463
- 'portable-async',
464
- ] as const)('bakes Site bounds and same-kind sibling reservations through %s hooks', async (mode) => {
465
- const restoreRegistry = nodeRegistry._snapshot()
466
- try {
467
- const kind = 'test:site-context-bake'
468
- const nodeId = 'site_context_bake'
469
- const site = SiteNode.parse({
470
- polygon: {
471
- type: 'polygon',
472
- points: [
473
- [0, 0],
474
- [12, 0],
475
- [12, 8],
476
- [0, 8],
477
- ],
478
- },
479
- children: [nodeId, 'declared_sibling', 'linked_sibling', 'other_kind'],
480
- })
481
- const otherSite = SiteNode.parse({
482
- polygon: {
483
- type: 'polygon',
484
- points: [
485
- [0, 0],
486
- [30, 0],
487
- [30, 4],
488
- [0, 4],
489
- ],
490
- },
491
- children: ['other_site_sibling'],
492
- })
493
- const buildGeometry = (context: GeometryContext) => {
494
- const points = (context.parent as SiteNode | null)?.polygon?.points ?? [[0, 0]]
495
- const xs = points.map(([x]) => x)
496
- const zs = points.map(([, z]) => z)
497
- const reservedWidth = context.siblings.reduce(
498
- (width, sibling) => width + Number(sibling.metadata?.reservedWidth ?? 0),
499
- 0,
500
- )
501
- return new THREE.Mesh(
502
- new THREE.BoxGeometry(
503
- Math.max(...xs) - Math.min(...xs) - reservedWidth,
504
- 1,
505
- Math.max(...zs) - Math.min(...zs),
506
- ),
507
- new THREE.MeshStandardMaterial(),
508
- )
509
- }
510
- registerNode({
511
- kind,
512
- schemaVersion: 1,
513
- schema: DoorNode,
514
- category: 'furnish',
515
- defaults: () => ({}) as never,
516
- capabilities: {},
517
- bake: 'replace',
518
- ...(mode === 'portable-async'
519
- ? { bakeGeometryAsync: async (_node, context) => buildGeometry(context) }
520
- : { bakeGeometry: (_node, context) => buildGeometry(context) }),
521
- } as AnyNodeDefinition)
522
- const nodes = {
523
- [site.id]: site,
524
- [otherSite.id]: otherSite,
525
- [nodeId]: {
526
- id: nodeId,
527
- type: kind,
528
- parentId: null,
529
- visible: true,
530
- metadata: { reservedWidth: 100 },
531
- },
532
- declared_sibling: {
533
- id: 'declared_sibling',
534
- type: kind,
535
- parentId: null,
536
- metadata: { reservedWidth: 2 },
537
- },
538
- pointer_sibling: {
539
- id: 'pointer_sibling',
540
- type: kind,
541
- parentId: site.id,
542
- metadata: { reservedWidth: 3 },
543
- },
544
- linked_sibling: {
545
- id: 'linked_sibling',
546
- type: kind,
547
- parentId: site.id,
548
- metadata: { reservedWidth: 1 },
549
- },
550
- other_kind: {
551
- id: 'other_kind',
552
- type: 'test:other-kind',
553
- parentId: site.id,
554
- metadata: { reservedWidth: 100 },
555
- },
556
- other_site_sibling: {
557
- id: 'other_site_sibling',
558
- type: kind,
559
- parentId: otherSite.id,
560
- metadata: { reservedWidth: 4 },
561
- },
562
- } as unknown as Record<string, AnyNode>
563
- const root = new THREE.Group()
564
- const source = new THREE.Group()
565
- root.add(source)
566
- sceneRegistry.nodes.set(nodeId, source)
567
- const prepare = mode === 'sync' ? prepareSceneForExport : prepareSceneForExportAsync
568
- const exportedSize = async () => {
569
- const prepared = await prepare(root, nodes)
570
- try {
571
- const exported = prepared.scene.getObjectByName(nodeId)!
572
- return new THREE.Box3().setFromObject(exported).getSize(new THREE.Vector3()).toArray()
573
- } finally {
574
- prepared.dispose()
460
+ test.each(['sync', 'portable-sync', 'portable-async'] as const)(
461
+ 'bakes Site bounds and same-kind sibling reservations through %s hooks',
462
+ async (mode) => {
463
+ const restoreRegistry = nodeRegistry._snapshot()
464
+ try {
465
+ const kind = 'test:site-context-bake'
466
+ const nodeId = 'site_context_bake'
467
+ const site = SiteNode.parse({
468
+ polygon: {
469
+ type: 'polygon',
470
+ points: [
471
+ [0, 0],
472
+ [12, 0],
473
+ [12, 8],
474
+ [0, 8],
475
+ ],
476
+ },
477
+ children: [nodeId, 'declared_sibling', 'linked_sibling', 'other_kind'],
478
+ })
479
+ const otherSite = SiteNode.parse({
480
+ polygon: {
481
+ type: 'polygon',
482
+ points: [
483
+ [0, 0],
484
+ [30, 0],
485
+ [30, 4],
486
+ [0, 4],
487
+ ],
488
+ },
489
+ children: ['other_site_sibling'],
490
+ })
491
+ const buildGeometry = (context: GeometryContext) => {
492
+ const points = (context.parent as SiteNode | null)?.polygon?.points ?? [[0, 0]]
493
+ const xs = points.map(([x]) => x)
494
+ const zs = points.map(([, z]) => z)
495
+ const reservedWidth = context.siblings.reduce(
496
+ (width, sibling) => width + Number(sibling.metadata?.reservedWidth ?? 0),
497
+ 0,
498
+ )
499
+ return new THREE.Mesh(
500
+ new THREE.BoxGeometry(
501
+ Math.max(...xs) - Math.min(...xs) - reservedWidth,
502
+ 1,
503
+ Math.max(...zs) - Math.min(...zs),
504
+ ),
505
+ new THREE.MeshStandardMaterial(),
506
+ )
507
+ }
508
+ registerNode({
509
+ kind,
510
+ schemaVersion: 1,
511
+ schema: DoorNode,
512
+ category: 'furnish',
513
+ defaults: () => ({}) as never,
514
+ capabilities: {},
515
+ bake: 'replace',
516
+ ...(mode === 'portable-async'
517
+ ? { bakeGeometryAsync: async (_node, context) => buildGeometry(context) }
518
+ : { bakeGeometry: (_node, context) => buildGeometry(context) }),
519
+ } as AnyNodeDefinition)
520
+ const nodes = {
521
+ [site.id]: site,
522
+ [otherSite.id]: otherSite,
523
+ [nodeId]: {
524
+ id: nodeId,
525
+ type: kind,
526
+ parentId: null,
527
+ visible: true,
528
+ metadata: { reservedWidth: 100 },
529
+ },
530
+ declared_sibling: {
531
+ id: 'declared_sibling',
532
+ type: kind,
533
+ parentId: null,
534
+ metadata: { reservedWidth: 2 },
535
+ },
536
+ pointer_sibling: {
537
+ id: 'pointer_sibling',
538
+ type: kind,
539
+ parentId: site.id,
540
+ metadata: { reservedWidth: 3 },
541
+ },
542
+ linked_sibling: {
543
+ id: 'linked_sibling',
544
+ type: kind,
545
+ parentId: site.id,
546
+ metadata: { reservedWidth: 1 },
547
+ },
548
+ other_kind: {
549
+ id: 'other_kind',
550
+ type: 'test:other-kind',
551
+ parentId: site.id,
552
+ metadata: { reservedWidth: 100 },
553
+ },
554
+ other_site_sibling: {
555
+ id: 'other_site_sibling',
556
+ type: kind,
557
+ parentId: otherSite.id,
558
+ metadata: { reservedWidth: 4 },
559
+ },
560
+ } as unknown as Record<string, AnyNode>
561
+ const root = new THREE.Group()
562
+ const source = new THREE.Group()
563
+ root.add(source)
564
+ sceneRegistry.nodes.set(nodeId, source)
565
+ const prepare = mode === 'sync' ? prepareSceneForExport : prepareSceneForExportAsync
566
+ const exportedSize = async () => {
567
+ const prepared = await prepare(root, nodes)
568
+ try {
569
+ const exported = prepared.scene.getObjectByName(nodeId)!
570
+ return new THREE.Box3().setFromObject(exported).getSize(new THREE.Vector3()).toArray()
571
+ } finally {
572
+ prepared.dispose()
573
+ }
575
574
  }
576
- }
577
575
 
578
- expect(await exportedSize()).toEqual([6, 1, 8])
579
- nodes[nodeId]!.parentId = otherSite.id
580
- expect(await exportedSize()).toEqual([26, 1, 4])
581
- nodes[nodeId]!.parentId = null
582
- site.children = site.children.filter((id) => id !== nodeId)
583
- expect(await exportedSize()).toEqual([0, 1, 0])
584
- } finally {
585
- restoreRegistry()
586
- }
587
- })
576
+ expect(await exportedSize()).toEqual([6, 1, 8])
577
+ nodes[nodeId]!.parentId = otherSite.id
578
+ expect(await exportedSize()).toEqual([26, 1, 4])
579
+ nodes[nodeId]!.parentId = null
580
+ site.children = site.children.filter((id) => id !== nodeId)
581
+ expect(await exportedSize()).toEqual([0, 1, 0])
582
+ } finally {
583
+ restoreRegistry()
584
+ }
585
+ },
586
+ )
588
587
 
589
588
  test('selective exports omit a procedural kind without changing the live scene or later exports', () => {
590
589
  const restoreRegistry = nodeRegistry._snapshot()
@@ -2373,29 +2372,32 @@ describe('plugin bake policies through export', () => {
2373
2372
  test.each([
2374
2373
  ['sync', 'meadow-bake', ['bakeGeometry']],
2375
2374
  ['async', 'meadow-bake-async', ['bakeGeometryAsync']],
2376
- ] as const)('%s export strips, replaces and keeps static plugin kinds without touching the live tree', async (mode, bakedName, expectedCalls) => {
2377
- await withBakePlugin([pluginId], async ({ root, live, nodes, calls }) => {
2378
- const prepared =
2379
- mode === 'sync'
2380
- ? prepareSceneForExport(root, nodes)
2381
- : await prepareSceneForExportAsync(root, nodes)
2382
- try {
2383
- const scene = prepared.scene
2384
- expect(scene.getObjectByName(overlayId)).toBeUndefined()
2385
- expect(scene.getObjectByName(`${overlayId}-live`)).toBeUndefined()
2375
+ ] as const)(
2376
+ '%s export strips, replaces and keeps static plugin kinds without touching the live tree',
2377
+ async (mode, bakedName, expectedCalls) => {
2378
+ await withBakePlugin([pluginId], async ({ root, live, nodes, calls }) => {
2379
+ const prepared =
2380
+ mode === 'sync'
2381
+ ? prepareSceneForExport(root, nodes)
2382
+ : await prepareSceneForExportAsync(root, nodes)
2383
+ try {
2384
+ const scene = prepared.scene
2385
+ expect(scene.getObjectByName(overlayId)).toBeUndefined()
2386
+ expect(scene.getObjectByName(`${overlayId}-live`)).toBeUndefined()
2386
2387
 
2387
- const meadow = scene.getObjectByName(meadowId)
2388
- expect(meadow?.getObjectByName(bakedName)).toBeDefined()
2389
- expect(meadow?.getObjectByName(`${meadowId}-live`)).toBeUndefined()
2388
+ const meadow = scene.getObjectByName(meadowId)
2389
+ expect(meadow?.getObjectByName(bakedName)).toBeDefined()
2390
+ expect(meadow?.getObjectByName(`${meadowId}-live`)).toBeUndefined()
2390
2391
 
2391
- expect(scene.getObjectByName(rockId)?.getObjectByName(`${rockId}-live`)).toBeDefined()
2392
- expect(calls).toEqual([...expectedCalls])
2393
- expectLiveTreeUntouched(live)
2394
- } finally {
2395
- prepared.dispose()
2396
- }
2397
- })
2398
- })
2392
+ expect(scene.getObjectByName(rockId)?.getObjectByName(`${rockId}-live`)).toBeDefined()
2393
+ expect(calls).toEqual([...expectedCalls])
2394
+ expectLiveTreeUntouched(live)
2395
+ } finally {
2396
+ prepared.dispose()
2397
+ }
2398
+ })
2399
+ },
2400
+ )
2399
2401
 
2400
2402
  test('an uninstalled plugin contributes nothing to the export', async () => {
2401
2403
  await withBakePlugin([], async ({ root, live, nodes, calls }) => {
@@ -2436,3 +2438,26 @@ test('prepared exports preserve surface identities and IFC subpart roles on regi
2436
2438
  expect(exported.userData.pascalIfcRole).toBe('railing')
2437
2439
  expect(exported.userData.pascalIfcParts).toEqual([{ start: 0, count: 3, role: 'handrail' }])
2438
2440
  })
2441
+
2442
+ test('exports a plugin LOD once without invoking its argument-requiring constructor', () => {
2443
+ class PluginLOD extends THREE.LOD {
2444
+ constructor(model: THREE.Mesh) {
2445
+ super()
2446
+ if (!model) throw new Error('Model is required')
2447
+ this.addLevel(model, 0)
2448
+ this.addLevel(model.clone(), 10)
2449
+ }
2450
+ }
2451
+ const mesh = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial())
2452
+ const lod = new PluginLOD(mesh)
2453
+ const root = new THREE.Group()
2454
+ root.add(lod)
2455
+ const { scene } = prepareSceneForExport(root, {})
2456
+ const meshes: THREE.Mesh[] = []
2457
+ scene.traverse((object) => {
2458
+ if (object instanceof THREE.Mesh) meshes.push(object)
2459
+ })
2460
+ expect(meshes).toHaveLength(2)
2461
+ expect(meshes[0]!.geometry).not.toBe(mesh.geometry)
2462
+ expect(lod.levels).toHaveLength(2)
2463
+ })
@@ -903,7 +903,10 @@ function cloneSceneForExport(
903
903
  ): THREE.Object3D {
904
904
  if (excludedObjects.has(source)) return new THREE.Group()
905
905
 
906
- const clone = source.clone(false)
906
+ // LOD.copy clones every level even when called through clone(false). Export
907
+ // walks the children itself, and plugin LOD constructors may require inputs.
908
+ const clone =
909
+ source instanceof THREE.LOD ? new THREE.Group().copy(source, false) : source.clone(false)
907
910
  clone.userData = cloneExportUserData(source.userData)
908
911
  const renderable = source as THREE.Mesh
909
912
  const renderableClone = clone as THREE.Mesh
@@ -109,11 +109,10 @@ describe('standalone history source invalidation', () => {
109
109
  `)
110
110
  })
111
111
 
112
- test.each([
113
- true,
114
- false,
115
- ])('discarded preview neighbours rebuild after undo/redo (joined: %s)', (joined) => {
116
- runSourceHistoryTest(`
112
+ test.each([true, false])(
113
+ 'discarded preview neighbours rebuild after undo/redo (joined: %s)',
114
+ (joined) => {
115
+ runSourceHistoryTest(`
117
116
  const react = await importShared('react')
118
117
  mockShared('react', () => ({ ...react, useEffect: () => {} }))
119
118
  const frames = []
@@ -152,7 +151,8 @@ describe('standalone history source invalidation', () => {
152
151
  assert.deepEqual(Array.from(b.geometry.getAttribute('position').array), canonical)
153
152
  }
154
153
  `)
155
- })
154
+ },
155
+ )
156
156
 
157
157
  test('opening reparent dirties old and new walls on undo and redo', () => {
158
158
  runSourceHistoryTest(`
@@ -343,7 +343,12 @@ describe('standalone history source invalidation', () => {
343
343
  mockShared('react', () => ({ ...react, useEffect: () => {}, useRef: current => ({ current }) }))
344
344
  const frames = []
345
345
  const fiber = await importShared('@react-three/fiber')
346
- mockShared('@react-three/fiber', () => ({ ...fiber, useFrame: frame => frames.push(frame) }))
346
+ // DoorSystem and WindowSystem register their materials for scene capture through useThree.
347
+ mockShared('@react-three/fiber', () => ({
348
+ ...fiber,
349
+ useFrame: frame => frames.push(frame),
350
+ useThree: select => select({ scene: {} }),
351
+ }))
347
352
  const selector = store => Object.assign(fn => fn(store.getState()), store)
348
353
  mockShared('@pascal-app/core', () => ({ ...core, useScene: selector(scene), useLiveNodeOverrides: selector(overrides) }))
349
354
  const viewer = await importShared('@pascal-app/viewer')
@@ -111,6 +111,10 @@ export type InteractionScope =
111
111
  // (`resolveOverlayPolicy`). A brush that only claimed the scope between
112
112
  // pointer-down and -up would flicker all of that back on between dabs.
113
113
  | { kind: 'sculpting' }
114
+ // Point and ask: the pointer belongs to pointing at things for the agent, so selection, handles and
115
+ // the floating action menu step back (the same active policy as a brush) for as long as the mode
116
+ // is on. Held by the point-ask session, which leaves the mode if another interaction takes over.
117
+ | { kind: 'pointing' }
114
118
 
115
119
  export type InteractionKind = InteractionScope['kind']
116
120
 
@@ -0,0 +1,57 @@
1
+ import { beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeId,
5
+ BuildingNode,
6
+ LevelNode,
7
+ RoofNode,
8
+ useScene,
9
+ WallNode,
10
+ } from '@pascal-app/core'
11
+ import { useViewer } from '@pascal-app/viewer'
12
+ import { selectPlacedNode } from './level-selection'
13
+
14
+ // A roof drawn over the ground floor's walls lands on the Roof level, which the editor hides while
15
+ // the ground floor is selected: selecting the roof there alone left it out of sight.
16
+ const BUILDING = 'building_select' as BuildingNode['id']
17
+ const GROUND = 'level_ground' as LevelNode['id']
18
+
19
+ beforeEach(() => {
20
+ const ground = LevelNode.parse({ id: GROUND, parentId: BUILDING, level: 0, children: ['wall_a'] })
21
+ const wall = WallNode.parse({ id: 'wall_a', parentId: GROUND, start: [0, 0], end: [4, 0] })
22
+ const roofLevel = LevelNode.parse({
23
+ id: 'level_roof',
24
+ parentId: BUILDING,
25
+ level: 1,
26
+ children: ['roof_main'],
27
+ metadata: { role: 'roof' },
28
+ })
29
+ const roof = RoofNode.parse({ id: 'roof_main', parentId: roofLevel.id })
30
+ const building = BuildingNode.parse({ id: BUILDING, children: [GROUND, roofLevel.id] })
31
+ useScene.setState({
32
+ nodes: Object.fromEntries(
33
+ [building, ground, wall, roofLevel, roof].map((node) => [node.id, node as AnyNode]),
34
+ ),
35
+ rootNodeIds: [BUILDING],
36
+ } as never)
37
+ useViewer.getState().setSelection({ buildingId: BUILDING, levelId: GROUND })
38
+ })
39
+
40
+ describe('selectPlacedNode', () => {
41
+ test('a node placed on another level is selected with its level', () => {
42
+ selectPlacedNode('roof_main' as AnyNodeId)
43
+ expect(useViewer.getState().selection).toMatchObject({
44
+ buildingId: BUILDING,
45
+ levelId: 'level_roof',
46
+ selectedIds: ['roof_main'],
47
+ })
48
+ })
49
+
50
+ test('a node placed on the level in view leaves the level as it is', () => {
51
+ selectPlacedNode('wall_a' as AnyNodeId)
52
+ expect(useViewer.getState().selection).toMatchObject({
53
+ levelId: GROUND,
54
+ selectedIds: ['wall_a'],
55
+ })
56
+ })
57
+ })