@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
@@ -1,5 +1,5 @@
1
1
  import type { AnyNodeId, BuildingNode, LevelNode } from '@pascal-app/core'
2
- import { useScene } from '@pascal-app/core'
2
+ import { findLevelAncestorId, useScene } from '@pascal-app/core'
3
3
  import { useViewer } from '@pascal-app/viewer'
4
4
 
5
5
  function getAdjacentLevelIdForDeletion(levelId: AnyNodeId): LevelNode['id'] | null {
@@ -29,3 +29,18 @@ export function deleteLevelWithFallbackSelection(levelId: AnyNodeId) {
29
29
  useViewer.getState().setSelection({ levelId: nextLevelId })
30
30
  }
31
31
  }
32
+
33
+ /**
34
+ * Selects a node a tool just made, on its own level when that is not the one in view: a roof drawn
35
+ * over a storey's walls lands on the level above, which the editor hides while the storey is
36
+ * selected, so selecting it there alone would leave it out of sight.
37
+ */
38
+ export function selectPlacedNode(nodeId: AnyNodeId) {
39
+ const levelId = findLevelAncestorId(nodeId, useScene.getState().nodes)
40
+ const { selection, setSelection } = useViewer.getState()
41
+ setSelection(
42
+ levelId && levelId !== selection.levelId
43
+ ? { levelId: levelId as LevelNode['id'], selectedIds: [nodeId] }
44
+ : { selectedIds: [nodeId] },
45
+ )
46
+ }
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
+ import { classifyEdges, setbackEnvelope } from '@pascal-app/core'
2
3
  import type { Pt } from '../floorplan/site-plan/geometry'
3
- import { classifyEdges, polygonArea, setbackEnvelope } from '../floorplan/site-plan/geometry'
4
+ import { polygonArea } from '../floorplan/site-plan/geometry'
4
5
  import {
5
6
  cleanLotRing,
6
7
  describeRingCleanup,
@@ -24,15 +24,12 @@
24
24
  import {
25
25
  type AnyNode,
26
26
  type AnyNodeId,
27
- type BuildingNode,
28
27
  generateId,
28
+ placeBuildingPose,
29
29
  SiteNode,
30
+ siteBrief,
30
31
  useScene,
31
32
  } from '@pascal-app/core'
32
- import {
33
- buildingRecentreOffset,
34
- buildSitePlanDrawing,
35
- } from '../floorplan/site-plan/build-site-plan-drawing'
36
33
  import {
37
34
  answered,
38
35
  type CodeBasisData,
@@ -339,27 +336,15 @@ export async function dropInLot(
339
336
  terrainFailure = 'skipped'
340
337
  }
341
338
 
342
- // Centre the building on the lot when its footprint fell outside the new ring.
339
+ // A house the new lot leaves past its lines stands centred on the buildable area again, facing
340
+ // as it did.
343
341
  let recentred = false
344
342
  const after = useScene.getState()
345
- const drawing = buildSitePlanDrawing({
346
- collections: after.collections,
347
- installedPlugins: after.installedPlugins,
348
- materials: after.materials,
349
- nodes: after.nodes,
350
- rootNodeIds: after.rootNodeIds,
351
- })
352
- const offset = buildingRecentreOffset(drawing.meta.lot, drawing.meta.footprintBounds)
353
- if (offset && drawing.meta.buildingId) {
354
- const building = after.nodes[drawing.meta.buildingId as AnyNodeId] as BuildingNode | undefined
355
- if (building) {
356
- after.updateNode(building.id, {
357
- position: [
358
- building.position[0] + offset[0],
359
- building.position[1],
360
- building.position[2] + offset[1],
361
- ],
362
- })
343
+ const brief = siteBrief(after.nodes)
344
+ if (brief?.buildingId && brief.footprint?.fit === 'outside') {
345
+ const placed = placeBuildingPose(after.nodes, { buildingId: brief.buildingId })
346
+ if (placed.ok && placed.moved) {
347
+ after.updateNode(placed.buildingId as AnyNodeId, { position: placed.position })
363
348
  recentred = true
364
349
  }
365
350
  }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { edgeLength } from '../floorplan/site-plan/geometry'
2
+ import { edgeLength } from '@pascal-app/core'
3
3
  import {
4
4
  DEFAULT_SETBACKS_M,
5
5
  DEFAULT_SETBACKS_SOURCE,
@@ -22,13 +22,13 @@
22
22
  * - `northRotation` is 0: the parcel frame is x east / z south, so plan up
23
23
  * is true north.
24
24
  */
25
- import type { SiteNode, SiteSetbacks } from '@pascal-app/core'
25
+ import { edgeLength, type SiteNode, type SiteSetbacks } from '@pascal-app/core'
26
26
  import {
27
27
  detectFrontEdgeFromRoads,
28
28
  type FrontEdgeMatch,
29
29
  type RoadCenterline,
30
30
  } from '../floorplan/site-plan/front-edge'
31
- import { edgeLength, METRES_PER_FOOT, type Pt } from '../floorplan/site-plan/geometry'
31
+ import { METRES_PER_FOOT, type Pt } from '../floorplan/site-plan/geometry'
32
32
  import { sameStreet } from '../floorplan/site-plan/street-name'
33
33
  import { cleanLotRing, describeRingCleanup } from './clean-ring'
34
34
  import { detectFrontEdgeFromFrontage, type FrontageMatch } from './dossier'
@@ -19,12 +19,10 @@ describe('creatable measurement kinds', () => {
19
19
  }
20
20
  })
21
21
 
22
- test.each([
23
- 'smart',
24
- 'unknown',
25
- null,
26
- undefined,
27
- ])('falls back from a non-creatable persisted value: %s', (value) => {
28
- expect(normalizeCreatableMeasurementKind(value)).toBe(DEFAULT_CREATABLE_MEASUREMENT_KIND)
29
- })
22
+ test.each(['smart', 'unknown', null, undefined])(
23
+ 'falls back from a non-creatable persisted value: %s',
24
+ (value) => {
25
+ expect(normalizeCreatableMeasurementKind(value)).toBe(DEFAULT_CREATABLE_MEASUREMENT_KIND)
26
+ },
27
+ )
30
28
  })
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
+ import { measurementHint, parseMeasurement } from '../index'
2
3
  import {
3
4
  buildMeasurementAngleArcPoints,
4
5
  cubicMetersToVolumeUnit,
@@ -190,3 +191,25 @@ describe('volume measurements', () => {
190
191
  expect(formatVolumeLabel(Number.NEGATIVE_INFINITY, 'metric')).toBe('--')
191
192
  })
192
193
  })
194
+
195
+ describe('typed measurement fields', () => {
196
+ test('canonicalizes explicit, compound, and scientific dimensions', () => {
197
+ const length = { kind: 'length', unitId: 'm' } as const
198
+ expect(parseMeasurement('180cm', length)).toBeCloseTo(1.8)
199
+ expect(parseMeasurement('1m80', length)).toBeCloseTo(1.8)
200
+ expect(parseMeasurement(`5'11"`, length)).toBeCloseTo(1.8034)
201
+ expect(parseMeasurement('1.234e-4 m', length)).toBeCloseTo(0.0001234, 10)
202
+ expect(parseMeasurement('6', length, { bareUnit: 'ft' })).toBeCloseTo(1.8288)
203
+ expect(parseMeasurement('180deg', { kind: 'angle', unitId: 'rad' })).toBeCloseTo(Math.PI)
204
+ })
205
+
206
+ test('rejects ranges, incomplete arithmetic, and nonfinite input', () => {
207
+ const length = { kind: 'length', unitId: 'm' } as const
208
+ for (const input of ['2+3 m', '2-3 m', 'between 2 and 3 m', '1e999 m']) {
209
+ expect(parseMeasurement(input, length)).toBeNull()
210
+ expect(measurementHint(input, length)).toBeNull()
211
+ }
212
+ expect(measurementHint('180cm', length)).toBe('= 1.80 m')
213
+ expect(measurementHint('6', length)).toBeNull()
214
+ })
215
+ })
@@ -1,5 +1,8 @@
1
1
  import { expect, test } from 'bun:test'
2
2
  import {
3
+ DIVE_DEPTH,
4
+ DIVE_MARGIN,
5
+ diveFloorY,
3
6
  floorLimitAt,
4
7
  glideFloorLimit,
5
8
  LOOK_UP_PITCH_GAIN,
@@ -7,8 +10,11 @@ import {
7
10
  MAX_ORBIT_POLAR_ANGLE,
8
11
  ORBIT_FLOOR_CLEARANCE,
9
12
  ORBIT_TARGET_HEIGHT,
13
+ OVERVIEW_GROUND_CLEARANCE,
10
14
  orbitAngleBelowFloor,
11
15
  orbitCameraAboveFloor,
16
+ orbitFloorMinY,
17
+ overviewEyeY,
12
18
  } from './orbit-floor-clearance'
13
19
 
14
20
  type Vec3 = { x: number; y: number; z: number }
@@ -163,3 +169,33 @@ test('a change in floor height is glided over, never popped', () => {
163
169
  }
164
170
  expect(limit).toBeCloseTo(0.75, 6)
165
171
  })
172
+
173
+ test('a pivot sent four storeys down keeps the eye where it was, over the ground; a diver stays under', () => {
174
+ // Eye 12 m up over a pivot at 1.1; the pivot goes to -12: the move alone would put the eye at -0.1.
175
+ expect(overviewEyeY(12 - 13.1, 12, -0.05, false)).toBe(12)
176
+ // An eye that was already low is brought up to the clearance.
177
+ expect(overviewEyeY(-9, 0.5, -0.05, false)).toBe(-0.05 + OVERVIEW_GROUND_CLEARANCE)
178
+ // The move leaves it high enough: it goes with the pivot, as always.
179
+ expect(overviewEyeY(20, 25, -0.05, false)).toBe(20)
180
+ // The person is under the ground: the move is theirs.
181
+ expect(overviewEyeY(-8, 12, -0.05, true)).toBe(-8)
182
+ })
183
+
184
+ test('the dive floor sits 3 m under the lowest ground, or 1 m under the deepest drawn thing when that is deeper', () => {
185
+ // A house at grade: nothing drawn is deeper than the ground's 3 m.
186
+ expect(diveFloorY(-0.05, -0.36)).toBeCloseTo(-0.05 - DIVE_DEPTH, 9)
187
+ expect(diveFloorY(-0.05, null)).toBeCloseTo(-0.05 - DIVE_DEPTH, 9)
188
+ // The villa's basement footing at -3.83 goes a metre deeper.
189
+ expect(diveFloorY(-0.05, -3.83)).toBeCloseTo(-3.83 - DIVE_MARGIN, 9)
190
+ })
191
+
192
+ test('the eye may sit the clearance over the dive floor from any storey, over the level floor otherwise', () => {
193
+ const dive = diveFloorY(-0.05, null)
194
+ // The ground floor, and a storey four metres up: both dive to the same floor.
195
+ expect(orbitFloorMinY(0, dive)).toBeCloseTo(dive + ORBIT_FLOOR_CLEARANCE, 9)
196
+ expect(orbitFloorMinY(4, dive)).toBeCloseTo(dive + ORBIT_FLOOR_CLEARANCE, 9)
197
+ // No dive (Solid): the level's floor, as before.
198
+ expect(orbitFloorMinY(4, null)).toBeCloseTo(4 + ORBIT_FLOOR_CLEARANCE, 9)
199
+ // A basement deeper than the dive floor keeps its own floor: the dive never lifts the limit.
200
+ expect(orbitFloorMinY(-12, dive)).toBeCloseTo(-12 + ORBIT_FLOOR_CLEARANCE, 9)
201
+ })
@@ -11,8 +11,31 @@ export const ORBIT_FLOOR_CLEARANCE = 0.45
11
11
  */
12
12
  export const ORBIT_TARGET_HEIGHT = 1.1
13
13
 
14
+ /**
15
+ * The dive under the ground: with the ground on Auto the orbit's eye may leave the ground for the
16
+ * earth under any building, down to a floor this far under the lowest ground, or this far under
17
+ * the deepest thing drawn (a basement's footing), whichever is deeper.
18
+ */
19
+ export const DIVE_DEPTH = 3
20
+ export const DIVE_MARGIN = 1
21
+
14
22
  const DEGREE = Math.PI / 180
15
23
 
24
+ /** The floor of the dive, world Y: the eye's clearance over it is {@link ORBIT_FLOOR_CLEARANCE}. */
25
+ export function diveFloorY(lowestGroundY: number, deepestDrawnY: number | null): number {
26
+ const underGround = lowestGroundY - DIVE_DEPTH
27
+ return deepestDrawnY === null ? underGround : Math.min(underGround, deepestDrawnY - DIVE_MARGIN)
28
+ }
29
+
30
+ /**
31
+ * The lowest the orbit's eye may sit: the clearance over the selected level's floor, or over the
32
+ * dive's floor when the person may dive and it lies deeper (`diveFloor` null: no dive). Selecting a
33
+ * level never lifts the limit above the dive floor, so a dive works from any storey.
34
+ */
35
+ export function orbitFloorMinY(levelFloorY: number, diveFloor: number | null): number {
36
+ return Math.min(levelFloorY, diveFloor ?? levelFloorY) + ORBIT_FLOOR_CLEARANCE
37
+ }
38
+
16
39
  /** The steepest an orbit camera looks: straight down is 0, straight up π. */
17
40
  export const MAX_ORBIT_POLAR_ANGLE = Math.PI - 0.05
18
41
 
@@ -104,3 +127,28 @@ export function glideFloorLimit(current: number | null, target: number, delta: n
104
127
  if (current === null || !(delta > 0)) return current ?? target
105
128
  return target + (current - target) * Math.exp(-delta / FLOOR_GLIDE_SECONDS)
106
129
  }
130
+
131
+ /**
132
+ * How far over the ground the overview's eye stays when the pivot goes down to a deep level: the
133
+ * ground is a medium, the eye above it sees the level through the hole and the dive is the
134
+ * person's own act, so picking a level four storeys under grade does not sink the eye with it.
135
+ */
136
+ export const OVERVIEW_GROUND_CLEARANCE = 2
137
+
138
+ /**
139
+ * The height an overview eye is kept at when its pivot moves to another level: `movedEyeY` as the
140
+ * move leaves it (the eye following the pivot down), unless that would sink the eye under the
141
+ * ground (the eye was above it, `wasUnder` false): then the eye stays where it was, over the ground
142
+ * (at least {@link OVERVIEW_GROUND_CLEARANCE} over it), and looks down at the new pivot. An eye
143
+ * already under the ground (the person dived) goes where the move puts it.
144
+ */
145
+ export function overviewEyeY(
146
+ movedEyeY: number,
147
+ previousEyeY: number,
148
+ groundY: number,
149
+ wasUnder: boolean,
150
+ ): number {
151
+ const lowest = groundY + OVERVIEW_GROUND_CLEARANCE
152
+ if (wasUnder || movedEyeY >= lowest) return movedEyeY
153
+ return Math.max(previousEyeY, lowest)
154
+ }
@@ -7,6 +7,7 @@ import {
7
7
  Group,
8
8
  type Intersection,
9
9
  Mesh,
10
+ MeshBasicMaterial,
10
11
  Path,
11
12
  Ray,
12
13
  Shape,
@@ -74,6 +75,16 @@ describe('paint passes through an empty opening', () => {
74
75
  it('without a drawn mesh the hit is the wall', () => {
75
76
  expect(wallHitInOpening(null, new Ray(), 1)).toBe(false)
76
77
  })
78
+
79
+ it('a drawn mesh with a group its material array has no entry for is the wall, without throwing', () => {
80
+ const torn = new Mesh(drawn.geometry.clone(), [new MeshBasicMaterial()])
81
+ torn.geometry.clearGroups()
82
+ torn.geometry.addGroup(0, Number.POSITIVE_INFINITY, 3)
83
+ torn.updateMatrixWorld(true)
84
+ const ray = new Ray(new Vector3(2, 1, 5), new Vector3(0, 0, -1))
85
+ expect(() => wallHitInOpening(torn, ray, 5 - 0.2)).not.toThrow()
86
+ expect(wallHitInOpening(torn, ray, 5 - 0.2)).toBe(false)
87
+ })
77
88
  })
78
89
 
79
90
  describe('a door part found behind the floor seen through its opening', () => {
@@ -1,5 +1,5 @@
1
1
  import type { AnyNode } from '@pascal-app/core'
2
- import { setSurfaceRaycastLayers } from '@pascal-app/viewer'
2
+ import { hasMaterialsForGroups, setSurfaceRaycastLayers } from '@pascal-app/viewer'
3
3
  import { type Intersection, Mesh, type Object3D, type Ray, Raycaster } from 'three'
4
4
 
5
5
  /**
@@ -62,7 +62,9 @@ export function wallHitInOpening(
62
62
  ray: Ray,
63
63
  hitDistance: number,
64
64
  ): boolean {
65
- if (!(drawn instanceof Mesh)) return false
65
+ // A mesh caught between a palette and the groups a preview gave it cannot be
66
+ // raycast (three reads each group's material); the hit stands this move.
67
+ if (!(drawn instanceof Mesh && hasMaterialsForGroups(drawn))) return false
66
68
  drawnRaycaster.ray.copy(ray)
67
69
  drawnRaycaster.far = Number.POSITIVE_INFINITY
68
70
  const hits: Intersection[] = []
@@ -337,6 +337,7 @@ function boundaryPointKey(point: readonly [number, number]): string {
337
337
  function boundarySegmentKey(boundary: WallBoundaryFace): string {
338
338
  const forward = boundary.points.map(boundaryPointKey).join('|')
339
339
  const reverse = [...boundary.points].reverse().map(boundaryPointKey).join('|')
340
+ // biome-ignore lint/complexity/useMathMinMax: lexicographic string comparison
340
341
  return `${boundary.wallId}:${forward < reverse ? forward : reverse}`
341
342
  }
342
343
 
@@ -6,13 +6,14 @@ import {
6
6
  useScene,
7
7
  } from '@pascal-app/core'
8
8
 
9
- export function commitParametricNodeFields(
9
+ /** A field edit with what its kind's parametrics derive and reconcile from it, as updates. */
10
+ export function parametricNodeUpdates(
10
11
  nodeId: AnyNodeId,
11
12
  requestedPatch: Partial<AnyNode>,
12
- ): void {
13
- const scene = useScene.getState()
14
- const node = scene.nodes[nodeId]
15
- if (!node) return
13
+ ): { id: AnyNodeId; data: Partial<AnyNode> }[] {
14
+ const nodes = useScene.getState().nodes
15
+ const node = nodes[nodeId]
16
+ if (!node) return []
16
17
 
17
18
  const parametrics = nodeRegistry.get(node.type)?.parametrics as
18
19
  | ParametricDescriptor<AnyNode>
@@ -31,7 +32,15 @@ export function commitParametricNodeFields(
31
32
  ]
32
33
  if (parametrics?.reconcile) {
33
34
  const next = { ...node, ...patch } as AnyNode
34
- updates.push(...parametrics.reconcile(node, next, { ...scene.nodes, [nodeId]: next }))
35
+ updates.push(...parametrics.reconcile(node, next, { ...nodes, [nodeId]: next }))
35
36
  }
36
- scene.updateNodes(updates)
37
+ return updates
38
+ }
39
+
40
+ export function commitParametricNodeFields(
41
+ nodeId: AnyNodeId,
42
+ requestedPatch: Partial<AnyNode>,
43
+ ): void {
44
+ const updates = parametricNodeUpdates(nodeId, requestedPatch)
45
+ if (updates.length) useScene.getState().updateNodes(updates)
37
46
  }
@@ -28,6 +28,19 @@ export type ChatToolCardProps = {
28
28
  }
29
29
  }
30
30
 
31
+ /**
32
+ * What a plugin's start screen does through the host's chat, before anything is said. `send` sends
33
+ * a message now, its pictures by reference (a host-stored image), and `attach` puts files on the
34
+ * next message, in the composer: both at the level that runs the plugin's tools. `requirePro` only
35
+ * asks whether the person's plan includes that level ("Pro" is the hosted product's name for it):
36
+ * it answers false after offering the upgrade, and changes nothing in the chat.
37
+ */
38
+ export type ChatStartProps = {
39
+ send(text: string, images?: readonly { url: string; name: string }[]): void
40
+ attach(files: readonly File[]): void
41
+ requirePro(): boolean
42
+ }
43
+
31
44
  /**
32
45
  * A plugin's contribution to a host's chat, exported from its browser entry beside its panels and
33
46
  * views; its tool contracts are its `./agent-tools` export. Which tools a turn offers, and to whom,
@@ -47,4 +60,6 @@ export type PluginChatExtension = {
47
60
  actions?: readonly { id: string; label: string; hint: string; prompt: string }[]
48
61
  /** File extensions or MIME types the plugin's tools read, which the composer then accepts. */
49
62
  accepts?: readonly string[]
63
+ /** What the chat shows while its thread is empty, loaded lazily. */
64
+ start?: () => Promise<{ default: ComponentType<ChatStartProps> }>
50
65
  }
@@ -0,0 +1,76 @@
1
+ import { useScene } from '@pascal-app/core'
2
+ import { act } from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { PerspectiveCamera, Raycaster } from 'three'
5
+ import { setEditorThreeContext } from '../../../components/editor/three-context-bridge'
6
+ import { PointAskLayer } from '../../../components/point-ask/point-ask-layer'
7
+ import { useAutoSave } from '../../../hooks/use-auto-save'
8
+ import useEditor from '../../../store/use-editor'
9
+ import { type PointBubble, usePointAsk } from '../../../store/use-point-ask'
10
+ import { changeMore, endCompare, redoAsk, sendBubble, startCompare, undoAsk } from '../session'
11
+
12
+ // Same-package entry: the private fixture uses this window boundary, never Editor src imports.
13
+ const saves: unknown[] = []
14
+ let saveControls: ReturnType<typeof useAutoSave>
15
+ function Host() {
16
+ saveControls = useAutoSave({
17
+ onSave: async (scene) => {
18
+ saves.push(structuredClone(scene))
19
+ },
20
+ })
21
+ return <PointAskLayer />
22
+ }
23
+ const element = document.createElement('div')
24
+ element.id = 'point-consumer-editor'
25
+ document.body.append(element)
26
+ const root = createRoot(element)
27
+ // Only the DOM mode precondition and CPU projection: no renderer or GPU is created.
28
+ const canvas = document.createElement('canvas')
29
+ canvas.width = 800
30
+ canvas.height = 600
31
+ document.body.append(canvas)
32
+ const camera = new PerspectiveCamera(50, 4 / 3, 0.1, 100)
33
+ camera.position.set(2, 5, 8)
34
+ camera.lookAt(2, 0, 2)
35
+ setEditorThreeContext({ camera, raycaster: new Raycaster(), domElement: canvas })
36
+ useEditor.setState({
37
+ viewMode: '3d',
38
+ workspaceMode: 'edit',
39
+ isPreviewMode: false,
40
+ isFirstPersonMode: false,
41
+ })
42
+ ;(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true
43
+ await act(async () => {
44
+ root.render(<Host />)
45
+ })
46
+
47
+ const fixture = {
48
+ scene: useScene,
49
+ point: usePointAsk,
50
+ act,
51
+ saves,
52
+ open: (bubble: PointBubble) =>
53
+ act(async () => {
54
+ usePointAsk.getState().openBubble(bubble)
55
+ }),
56
+ send: (text: string) =>
57
+ act(async () => {
58
+ await sendBubble(text, { x: 0, y: 0, width: 100, height: 20 })
59
+ }),
60
+ changeMore,
61
+ undo: undoAsk,
62
+ redo: redoAsk,
63
+ startCompare,
64
+ endCompare,
65
+ beginLoad: () => saveControls.beginSceneLoad(),
66
+ completeLoad: () => saveControls.completeSceneLoad(),
67
+ saveNow: () => saveControls.saveNow(),
68
+ unmount: () =>
69
+ act(async () => {
70
+ root.unmount()
71
+ setEditorThreeContext(null)
72
+ canvas.remove()
73
+ }),
74
+ }
75
+ ;(window as unknown as { pointEditorFixture: typeof fixture }).pointEditorFixture = fixture
76
+ window.dispatchEvent(new Event('point-editor-fixture-ready'))
@@ -0,0 +1,81 @@
1
+ import {
2
+ type AnyNode,
3
+ ItemNode,
4
+ LevelNode,
5
+ WallNode,
6
+ WindowNode,
7
+ ZoneNode,
8
+ DoorNode,
9
+ } from '@pascal-app/core'
10
+
11
+ // One 4 × 3 m kitchen on a ground level, as the point-ask tests share it: four walls, the room they
12
+ // make, a window in the north wall, a door in the south wall and a sofa on the floor.
13
+ export const LEVEL_ID = 'level_0'
14
+
15
+ const wall = (id: string, start: [number, number], end: [number, number]) =>
16
+ WallNode.parse({ id, parentId: LEVEL_ID, start, end, height: 2.6 })
17
+
18
+ const walls = [
19
+ wall('wall_north', [0, 0], [4, 0]),
20
+ wall('wall_east', [4, 0], [4, 3]),
21
+ wall('wall_south', [4, 3], [0, 3]),
22
+ wall('wall_west', [0, 3], [0, 0]),
23
+ ]
24
+
25
+ const zone = ZoneNode.parse({
26
+ id: 'zone_kitchen',
27
+ parentId: LEVEL_ID,
28
+ name: 'Kitchen',
29
+ polygon: [
30
+ [0, 0],
31
+ [4, 0],
32
+ [4, 3],
33
+ [0, 3],
34
+ ],
35
+ boundaryWallIds: walls.map((w) => w.id),
36
+ })
37
+
38
+ const window = WindowNode.parse({
39
+ id: 'window_north',
40
+ parentId: 'wall_north',
41
+ wallId: 'wall_north',
42
+ position: [2, 1.4, 0],
43
+ width: 1.2,
44
+ height: 1.4,
45
+ })
46
+
47
+ const door = DoorNode.parse({
48
+ id: 'door_front',
49
+ parentId: 'wall_south',
50
+ wallId: 'wall_south',
51
+ position: [2, 1.05, 0],
52
+ width: 0.9,
53
+ height: 2.1,
54
+ })
55
+
56
+ const sofa = ItemNode.parse({
57
+ id: 'item_sofa',
58
+ parentId: LEVEL_ID,
59
+ name: 'Sofa',
60
+ position: [1.5, 0, 1.5],
61
+ asset: {
62
+ id: 'sofa',
63
+ category: 'furniture',
64
+ name: 'Oslo 3-seat',
65
+ thumbnail: '',
66
+ src: 'asset://sofa.glb',
67
+ dimensions: [2.1, 0.85, 0.95],
68
+ },
69
+ })
70
+
71
+ const level = LevelNode.parse({
72
+ id: LEVEL_ID,
73
+ children: [...walls.map((w) => w.id), zone.id, sofa.id],
74
+ })
75
+
76
+ /** The scene as `useScene` holds it: nodes by id. */
77
+ export function house(extra: AnyNode[] = []): Record<string, AnyNode> {
78
+ return Object.fromEntries(
79
+ [level, ...walls, zone, window, door, sofa, ...extra].map((node) => [node.id, node as AnyNode]),
80
+ )
81
+ }