@pascal-app/editor 1.0.3 → 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 (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -34,17 +34,18 @@ export function AudioSettingsDialog() {
34
34
  <DialogContent className="sm:max-w-[425px]">
35
35
  <DialogHeader>
36
36
  <DialogTitle>Audio settings</DialogTitle>
37
- <DialogDescription>Adjust volume levels and mute settings</DialogDescription>
37
+ <DialogDescription>
38
+ Volume levels apply to every tab. Muting applies to this tab only.
39
+ </DialogDescription>
38
40
  </DialogHeader>
39
41
  <div className="space-y-6 py-4">
40
42
  {/* Master Volume */}
41
43
  <div className="space-y-2">
42
44
  <div className="flex items-center justify-between">
43
- <label className="font-medium text-sm">Master Volume</label>
45
+ <label className="font-medium text-sm">Master volume</label>
44
46
  <span className="text-muted-foreground text-sm">{masterVolume}%</span>
45
47
  </div>
46
48
  <Slider
47
- disabled={muted}
48
49
  max={100}
49
50
  onValueChange={(value) => value[0] !== undefined && setMasterVolume(value[0])}
50
51
  step={1}
@@ -55,11 +56,10 @@ export function AudioSettingsDialog() {
55
56
  {/* Radio Volume */}
56
57
  <div className="space-y-2">
57
58
  <div className="flex items-center justify-between">
58
- <label className="font-medium text-sm">Radio Volume</label>
59
+ <label className="font-medium text-sm">Radio volume</label>
59
60
  <span className="text-muted-foreground text-sm">{radioVolume}%</span>
60
61
  </div>
61
62
  <Slider
62
- disabled={muted}
63
63
  max={100}
64
64
  onValueChange={(value) => value[0] !== undefined && setRadioVolume(value[0])}
65
65
  step={1}
@@ -70,11 +70,10 @@ export function AudioSettingsDialog() {
70
70
  {/* SFX Volume */}
71
71
  <div className="space-y-2">
72
72
  <div className="flex items-center justify-between">
73
- <label className="font-medium text-sm">Sound Effects</label>
73
+ <label className="font-medium text-sm">Sound effects</label>
74
74
  <span className="text-muted-foreground text-sm">{sfxVolume}%</span>
75
75
  </div>
76
76
  <Slider
77
- disabled={muted}
78
77
  max={100}
79
78
  onValueChange={(value) => value[0] !== undefined && setSfxVolume(value[0])}
80
79
  step={1}
@@ -90,7 +89,7 @@ export function AudioSettingsDialog() {
90
89
  variant={muted ? 'default' : 'outline'}
91
90
  >
92
91
  {muted ? <VolumeX className="size-4" /> : <Volume2 className="size-4" />}
93
- {muted ? 'Unmute All Sounds' : 'Mute All Sounds'}
92
+ {muted ? 'Unmute this tab' : 'Mute this tab'}
94
93
  </Button>
95
94
  </div>
96
95
  </div>
@@ -0,0 +1,170 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNodeDefinition,
4
+ BaseNode,
5
+ loadPlugin,
6
+ nodeRegistry,
7
+ nodeType,
8
+ objectId,
9
+ useScene,
10
+ } from '@pascal-app/core'
11
+ import type { ConstructionRevealLevel } from '@pascal-app/viewer'
12
+ import { renderToStaticMarkup } from 'react-dom/server'
13
+ import useConstructionReveal, {
14
+ chooseConstructionLevel,
15
+ FRAMING_KIND,
16
+ resolveConstructionLevel,
17
+ } from '../../../../../store/use-construction-reveal'
18
+ import { AnimationSpeedSetting } from './animation-speed-setting'
19
+ import {
20
+ ConstructionAnimationSetting,
21
+ constructionLevelOptions,
22
+ FRAMING_NEEDS_ARCHITECT,
23
+ } from './construction-animation-setting'
24
+
25
+ // The framing plugin carries the Framing level: without it in the project the choice is greyed out with the
26
+ // reason as its tip, and the level that plays is Full.
27
+
28
+ const framing = {
29
+ kind: FRAMING_KIND,
30
+ schemaVersion: 1,
31
+ schema: BaseNode.extend({ id: objectId('framing'), type: nodeType(FRAMING_KIND) }),
32
+ category: 'structure',
33
+ defaults: () => ({}),
34
+ capabilities: {},
35
+ geometry: () => null,
36
+ } as unknown as AnyNodeDefinition
37
+
38
+ let restore: () => void = () => {}
39
+ beforeAll(async () => {
40
+ restore = nodeRegistry._snapshot()
41
+ nodeRegistry._reset()
42
+ await loadPlugin({ id: 'fixture:framing', apiVersion: 1, nodes: [framing] })
43
+ })
44
+ afterAll(() => restore())
45
+
46
+ // A server render reads a store's initial state, so the state to draw is put there.
47
+ const sceneInitial = useScene.getInitialState() as { installedPlugins: string[] }
48
+ const revealInitial = useConstructionReveal.getInitialState() as {
49
+ level: string
50
+ chosen: boolean
51
+ speed: number
52
+ }
53
+ const was = { plugins: sceneInitial.installedPlugins, ...revealInitial }
54
+
55
+ function project(options: { installed: boolean; level?: string; chosen?: boolean; speed?: number }) {
56
+ sceneInitial.installedPlugins = options.installed ? ['fixture:framing'] : []
57
+ revealInitial.level = options.level ?? 'full'
58
+ revealInitial.chosen = options.chosen ?? false
59
+ revealInitial.speed = options.speed ?? 1
60
+ }
61
+
62
+ beforeEach(() => project({ installed: false }))
63
+ afterEach(() => {
64
+ sceneInitial.installedPlugins = was.plugins
65
+ Object.assign(revealInitial, { level: was.level, chosen: was.chosen, speed: was.speed })
66
+ })
67
+
68
+ const optionOf = (markup: string, label: string) => {
69
+ const match = new RegExp(`<button[^>]*>(?:(?!</button>).)*${label}(?:(?!</button>).)*</button>`).exec(
70
+ markup,
71
+ )
72
+ return match?.[0] ?? ''
73
+ }
74
+
75
+ describe('the levels the control offers', () => {
76
+ test('without the plugin, Framing is disabled and its tip says it needs the Architect plugin', () => {
77
+ const framingOption = constructionLevelOptions(false).find((o) => o.value === 'framing')!
78
+ expect(framingOption.disabled).toBe(true)
79
+ expect(framingOption.title).toBe(FRAMING_NEEDS_ARCHITECT)
80
+ expect(FRAMING_NEEDS_ARCHITECT).toBe('Framing needs the Architect plugin.')
81
+ for (const option of constructionLevelOptions(false).filter((o) => o.value !== 'framing'))
82
+ expect(option.disabled).toBe(false)
83
+ })
84
+
85
+ test('with the plugin, every level is on offer', () => {
86
+ expect(constructionLevelOptions(true).every((option) => option.disabled === false)).toBe(true)
87
+ })
88
+ })
89
+
90
+ describe('the construction animation control, drawn', () => {
91
+ test('without the plugin: Framing is greyed out and Full is the level that plays', () => {
92
+ project({ installed: false })
93
+ const markup = renderToStaticMarkup(<ConstructionAnimationSetting />)
94
+ expect(optionOf(markup, 'Framing')).toContain('aria-disabled="true"')
95
+ expect(optionOf(markup, 'Full')).toContain('aria-pressed="true"')
96
+ expect(optionOf(markup, 'Full')).not.toContain('aria-disabled')
97
+ })
98
+
99
+ test('with the plugin and no choice of the person’s own: Framing plays, nothing is greyed', () => {
100
+ project({ installed: true })
101
+ const markup = renderToStaticMarkup(<ConstructionAnimationSetting />)
102
+ expect(optionOf(markup, 'Framing')).toContain('aria-pressed="true"')
103
+ expect(markup).not.toContain('aria-disabled')
104
+ })
105
+
106
+ test('with the plugin and a choice of Full: Full is kept', () => {
107
+ project({ installed: true, level: 'full', chosen: true })
108
+ const markup = renderToStaticMarkup(<ConstructionAnimationSetting />)
109
+ expect(optionOf(markup, 'Full')).toContain('aria-pressed="true"')
110
+ expect(optionOf(markup, 'Framing')).toContain('aria-pressed="false"')
111
+ })
112
+
113
+ test('a saved choice of Framing falls back to Full while the plugin is gone, greyed out', () => {
114
+ project({ installed: false, level: 'framing', chosen: true })
115
+ const markup = renderToStaticMarkup(<ConstructionAnimationSetting />)
116
+ expect(optionOf(markup, 'Full')).toContain('aria-pressed="true"')
117
+ expect(optionOf(markup, 'Framing')).toContain('aria-disabled="true"')
118
+ expect(optionOf(markup, 'Framing')).toContain('aria-pressed="false"')
119
+ })
120
+ })
121
+
122
+ describe('clicking the level that plays', () => {
123
+ // A saved Framing plays as Full while the plugin is gone; the control shows Full pressed, and a click on
124
+ // the pressed option is still a click. It must not turn the kept Framing into a chosen Full.
125
+ const click = (level: ConstructionRevealLevel, installed: boolean) =>
126
+ chooseConstructionLevel(level, installed)
127
+
128
+ beforeEach(() => useConstructionReveal.setState({ level: 'framing', chosen: true }))
129
+ afterEach(() => useConstructionReveal.setState({ level: 'full', chosen: false }))
130
+
131
+ test('the Full shown for a saved Framing keeps the Framing when it is clicked, and it plays again with the plugin', () => {
132
+ click('full', false)
133
+ expect(useConstructionReveal.getState()).toMatchObject({ level: 'framing', chosen: true })
134
+ expect(resolveConstructionLevel(useConstructionReveal.getState(), true)).toBe('framing')
135
+ })
136
+
137
+ test('another level is a choice and replaces it', () => {
138
+ click('simple', false)
139
+ expect(useConstructionReveal.getState()).toMatchObject({ level: 'simple', chosen: true })
140
+ })
141
+
142
+ test('with no choice of the person’s own, a click on the level that plays is a choice of it', () => {
143
+ useConstructionReveal.setState({ level: 'full', chosen: false })
144
+ click('full', false)
145
+ expect(useConstructionReveal.getState()).toMatchObject({ level: 'full', chosen: true })
146
+ })
147
+ })
148
+
149
+ describe('the Pro badge', () => {
150
+ // The plugin that draws the framing is a Pro one: the option carries the plugin manager's mark,
151
+ // greyed out or not, and no other level does.
152
+ test('Framing carries it, greyed out or on offer; the other levels do not', () => {
153
+ for (const installed of [false, true]) {
154
+ project({ installed })
155
+ const markup = renderToStaticMarkup(<ConstructionAnimationSetting />)
156
+ expect(optionOf(markup, 'Framing')).toMatch(/>Pro</)
157
+ for (const label of ['Off', 'Simple', 'Full']) expect(optionOf(markup, label)).not.toMatch(/>Pro</)
158
+ }
159
+ })
160
+ })
161
+
162
+ describe('the animation speed control, drawn', () => {
163
+ test('offers the five steps and shows the saved one', () => {
164
+ project({ installed: true, speed: 1.5 })
165
+ const markup = renderToStaticMarkup(<AnimationSpeedSetting />)
166
+ for (const label of ['×0.5', '×0.75', '×1', '×1.5', '×2']) expect(markup).toContain(label)
167
+ expect(optionOf(markup, '×1.5')).toContain('aria-pressed="true"')
168
+ expect(optionOf(markup, '×0.5')).toContain('aria-pressed="false"')
169
+ })
170
+ })
@@ -0,0 +1,92 @@
1
+ 'use client'
2
+
3
+ import type { ConstructionRevealLevel } from '@pascal-app/viewer'
4
+ import {
5
+ chooseConstructionLevel,
6
+ useConstructionRevealLevel,
7
+ useFramingAvailable,
8
+ } from '../../../../../store/use-construction-reveal'
9
+ import { SegmentedControl } from '../../../controls/segmented-control'
10
+ import { Badge } from '../../../primitives/badge'
11
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../../../primitives/tooltip'
12
+
13
+ /** Why Framing is greyed out. */
14
+ export const FRAMING_NEEDS_ARCHITECT = 'Framing needs the Architect plugin.'
15
+
16
+ const OPTIONS: { label: string; value: ConstructionRevealLevel; title: string; badge?: string }[] = [
17
+ { label: 'Off', value: 'off', title: 'What the AI builds appears at once.' },
18
+ {
19
+ label: 'Simple',
20
+ value: 'simple',
21
+ title: 'Walls rise and the rest grows in, piece by piece. No falls, no dust, no sound.',
22
+ },
23
+ {
24
+ label: 'Full',
25
+ value: 'full',
26
+ title: 'Pieces drop into place with dust, the roof assembles, one sound per phase.',
27
+ },
28
+ {
29
+ label: 'Framing',
30
+ value: 'framing',
31
+ title: 'Full, plus the framing members dropping in first, where the framing plugin draws them.',
32
+ // The plugin that draws the framing is a Pro one.
33
+ badge: 'Pro',
34
+ },
35
+ ]
36
+
37
+ /**
38
+ * The four levels as the control offers them: Framing is greyed out, with the reason as its tip,
39
+ * where the project has no plugin that draws the framing.
40
+ */
41
+ export function constructionLevelOptions(framingAvailable: boolean) {
42
+ return OPTIONS.map((option) =>
43
+ option.value === 'framing' && !framingAvailable
44
+ ? { ...option, title: FRAMING_NEEDS_ARCHITECT, disabled: true }
45
+ : { ...option, disabled: false },
46
+ )
47
+ }
48
+
49
+ /** How much of an AI build plays as a construction. */
50
+ export function ConstructionAnimationSetting() {
51
+ // What plays: the person's choice, or Framing where the framing plugin is installed and Full where it is not.
52
+ const level = useConstructionRevealLevel()
53
+ const framingAvailable = useFramingAvailable()
54
+ const current = OPTIONS.find((option) => option.value === level)
55
+ const options = constructionLevelOptions(framingAvailable).map((option) => {
56
+ const label = option.badge ? (
57
+ <span className="inline-flex items-center gap-1.5">
58
+ {option.label}
59
+ <Badge label={option.badge} />
60
+ </span>
61
+ ) : (
62
+ option.label
63
+ )
64
+ if (!option.disabled) return { ...option, label }
65
+ return {
66
+ ...option,
67
+ title: undefined,
68
+ label: (
69
+ <Tooltip>
70
+ <TooltipTrigger asChild>
71
+ <span>{label}</span>
72
+ </TooltipTrigger>
73
+ <TooltipContent>{option.title}</TooltipContent>
74
+ </Tooltip>
75
+ ),
76
+ }
77
+ })
78
+ return (
79
+ <div className="space-y-2 py-1">
80
+ <div className="space-y-0.5">
81
+ <p className="font-medium text-sm">Construction animation</p>
82
+ <p className="text-muted-foreground text-xs">{current?.title}</p>
83
+ </div>
84
+ <SegmentedControl
85
+ aria-label="Construction animation"
86
+ onChange={(next) => chooseConstructionLevel(next, framingAvailable)}
87
+ options={options}
88
+ value={level}
89
+ />
90
+ </div>
91
+ )
92
+ }
@@ -1,7 +1,10 @@
1
+ import { downloadProjectFile } from '../../../../../lib/download-project-file'
1
2
  import {
3
+ getProjectFileSink,
2
4
  clearSceneHistory,
3
5
  DEFAULT_LEVEL_HEIGHT,
4
6
  emitter,
7
+ getArtifactStore,
5
8
  getLevelDisplayName,
6
9
  LevelNode,
7
10
  type AnyNodeId,
@@ -25,9 +28,12 @@ import {
25
28
  Layers,
26
29
  ListTree,
27
30
  Map as MapIcon,
31
+ Monitor,
32
+ Moon,
28
33
  Save,
29
34
  ChevronDown,
30
35
  Send,
36
+ Sun,
31
37
  Trash2,
32
38
  Upload,
33
39
  } from 'lucide-react'
@@ -54,6 +60,7 @@ import {
54
60
  sendGlbToLocalApp,
55
61
  waitForLocalImport,
56
62
  } from '../../../../../lib/send-to-app'
63
+ import { SegmentedControl } from './../../../../../components/ui/controls/segmented-control'
57
64
  import { Button } from './../../../../../components/ui/primitives/button'
58
65
  import {
59
66
  Dialog,
@@ -64,11 +71,16 @@ import {
64
71
  import { Input } from './../../../../../components/ui/primitives/input'
65
72
  import { Switch } from './../../../../../components/ui/primitives/switch'
66
73
  import { cn } from './../../../../../lib/utils'
74
+ import { refusedObjectsNotice } from './../../../../../components/editor/group-actions'
75
+ import { addStoreyBelow, addStoreyOnTop } from './../../../../../lib/add-storey'
67
76
  import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection'
77
+ import { bringBuildArtifacts } from './../../../../../lib/scene-clipboard'
68
78
  import useEditor, { selectDefaultBuildingAndLevel } from './../../../../../store/use-editor'
69
79
  import useFloorplanMode from './../../../../../store/use-floorplan-mode'
70
80
  import { type SendToAppStep, useSendToApp } from './../../../../../store/use-send-to-app'
71
81
  import { AudioSettingsDialog } from './audio-settings-dialog'
82
+ import { AnimationSpeedSetting } from './animation-speed-setting'
83
+ import { ConstructionAnimationSetting } from './construction-animation-setting'
72
84
  import { KeyboardShortcutsDialog } from './keyboard-shortcuts-dialog'
73
85
  import { LoadBuildDialog, type PendingImport } from './load-build-dialog'
74
86
  import { PrintExportButton } from './print-export-button'
@@ -101,6 +113,7 @@ const MODEL_EXPORT_FORMATS = [
101
113
  { format: 'usdz', label: 'USDZ' },
102
114
  { format: 'stl', label: 'STL' },
103
115
  { format: 'obj', label: 'OBJ' },
116
+ { format: 'ifc', label: 'IFC' },
104
117
  ] as const
105
118
 
106
119
  type ModelExportFormat = (typeof MODEL_EXPORT_FORMATS)[number]['format']
@@ -271,7 +284,8 @@ function SettingsSubheading({ children }: { children: ReactNode }) {
271
284
  return <h4 className="font-medium text-muted-foreground text-xs">{children}</h4>
272
285
  }
273
286
 
274
- function SettingsSwitchRow({
287
+ /** One on/off preference: a label, a line on what it does, a switch. Hosts use it for their rows. */
288
+ export function SettingsSwitchRow({
275
289
  checked,
276
290
  description,
277
291
  label,
@@ -296,6 +310,65 @@ function SettingsSwitchRow({
296
310
  )
297
311
  }
298
312
 
313
+ export type InterfaceTheme = 'system' | 'light' | 'dark'
314
+
315
+ const INTERFACE_THEME_OPTIONS: { label: ReactNode; value: InterfaceTheme }[] = [
316
+ {
317
+ label: (
318
+ <>
319
+ <Monitor className="size-3.5" />
320
+ System
321
+ </>
322
+ ),
323
+ value: 'system',
324
+ },
325
+ {
326
+ label: (
327
+ <>
328
+ <Sun className="size-3.5" />
329
+ Light
330
+ </>
331
+ ),
332
+ value: 'light',
333
+ },
334
+ {
335
+ label: (
336
+ <>
337
+ <Moon className="size-3.5" />
338
+ Dark
339
+ </>
340
+ ),
341
+ value: 'dark',
342
+ },
343
+ ]
344
+
345
+ /**
346
+ * The interface theme as a System / Light / Dark row. The editor applies no theme itself: its
347
+ * chrome follows the host's `.dark` class, so a host that owns the setting passes this row.
348
+ */
349
+ export function InterfaceThemeRow({
350
+ onChange,
351
+ value,
352
+ }: {
353
+ onChange: (value: InterfaceTheme) => void
354
+ value: InterfaceTheme
355
+ }) {
356
+ const labelId = useId()
357
+ return (
358
+ <div className="flex flex-col gap-2 py-1 @sm:flex-row @sm:items-center @sm:justify-between @sm:gap-4">
359
+ <div className="min-w-0 space-y-0.5">
360
+ <p className="font-medium text-sm" id={labelId}>
361
+ Interface theme
362
+ </p>
363
+ <p className="text-muted-foreground text-xs">System follows your device’s appearance.</p>
364
+ </div>
365
+ <div aria-labelledby={labelId} className="w-full shrink-0 @sm:w-60" role="group">
366
+ <SegmentedControl onChange={onChange} options={INTERFACE_THEME_OPTIONS} value={value} />
367
+ </div>
368
+ </div>
369
+ )
370
+ }
371
+
299
372
  export interface SettingsPanelProps {
300
373
  /** Merged onto the scrolling root so hosts can restyle padding (e.g. inside a dialog). */
301
374
  className?: string
@@ -307,6 +380,11 @@ export interface SettingsPanelProps {
307
380
  field: 'isPrivate' | 'showScansPublic' | 'showGuidesPublic',
308
381
  value: boolean,
309
382
  ) => Promise<void>
383
+ /**
384
+ * Rows a host adds under Preferences, for its own features and settings (SettingsSwitchRow,
385
+ * InterfaceThemeRow).
386
+ */
387
+ preferences?: ReactNode
310
388
  }
311
389
 
312
390
  function FloorSettingsSection() {
@@ -342,20 +420,14 @@ function FloorSettingsSection() {
342
420
 
343
421
  if (!building) return null
344
422
 
345
- const addLevel = (level: number) => {
346
- const newLevel = LevelNode.parse({
347
- level,
348
- height: DEFAULT_LEVEL_HEIGHT,
349
- children: [],
350
- parentId: building.id,
351
- })
352
- createNode(newLevel, building.id)
423
+ const addFloor = () => {
424
+ const newLevel = addStoreyOnTop(building.id)
425
+ setSelection({ buildingId: building.id, levelId: newLevel.id })
426
+ }
427
+ const addBasement = () => {
428
+ const newLevel = addStoreyBelow(building.id)
353
429
  setSelection({ buildingId: building.id, levelId: newLevel.id })
354
430
  }
355
-
356
- const addFloor = () =>
357
- addLevel(levels.length === 0 ? 0 : Math.max(0, ...levels.map((level) => level.level)) + 1)
358
- const addBasement = () => addLevel(Math.min(0, ...levels.map((level) => level.level)) - 1)
359
431
 
360
432
  return (
361
433
  <div className="space-y-3">
@@ -431,6 +503,7 @@ export function SettingsPanel({
431
503
  projectName,
432
504
  projectVisibility,
433
505
  onVisibilityChange,
506
+ preferences,
434
507
  }: SettingsPanelProps = {}) {
435
508
  const fileInputRef = useRef<HTMLInputElement>(null)
436
509
  const copyResetTimeoutRef = useRef<number | null>(null)
@@ -540,24 +613,32 @@ export function SettingsPanel({
540
613
  // Materials ride along: nodes reference them by `scene:<id>` slot
541
614
  // refs, so a save without the table produces a file whose custom
542
615
  // finishes revert to defaults on the very Load Build path below.
543
- const sceneData = { nodes, rootNodeIds, installedPlugins, materials, collections }
616
+ // The project rides along too: a scripted object's artifacts live there, and
617
+ // loading the file into another project copies them over as a paste does.
618
+ const sceneData = {
619
+ nodes,
620
+ rootNodeIds,
621
+ installedPlugins,
622
+ materials,
623
+ collections,
624
+ projectId: useViewer.getState().projectId,
625
+ }
544
626
  const json = JSON.stringify(sceneData, null, 2)
545
627
  const blob = new Blob([json], { type: 'application/json' })
546
- const url = URL.createObjectURL(blob)
547
- const link = document.createElement('a')
548
- link.href = url
549
628
  const date = new Date().toISOString().split('T')[0]
550
- link.download = `layout_${date}.json`
551
- link.click()
552
- URL.revokeObjectURL(url)
629
+ setModelExportError(null)
630
+ void downloadProjectFile(blob, `layout_${date}.json`).catch((error: unknown) => {
631
+ setModelExportError(error instanceof Error ? error.message : 'Could not save the file')
632
+ })
553
633
  }
554
634
 
555
635
  const handleFileLoad = (e: React.ChangeEvent<HTMLInputElement>) => {
556
636
  const file = e.target.files?.[0]
557
637
  if (!file) return
558
638
 
639
+ const fileSink = getProjectFileSink()
559
640
  const reader = new FileReader()
560
- reader.onload = (event) => {
641
+ reader.onload = async (event) => {
561
642
  const text = event.target?.result as string
562
643
  let parsed: unknown
563
644
  try {
@@ -584,10 +665,20 @@ export function SettingsPanel({
584
665
  })
585
666
  return
586
667
  }
668
+ const result = validateBuildJson(parsed)
669
+ if (result.ok) {
670
+ try {
671
+ await fileSink?.({ blob: file, name: file.name })
672
+ } catch (error) {
673
+ setModelExportError(error instanceof Error ? error.message : 'Could not save the source file')
674
+ return
675
+ }
676
+ if (fileSink !== getProjectFileSink()) return
677
+ }
587
678
  setPendingImport({
588
679
  fileName: file.name,
589
680
  fileSizeBytes: file.size,
590
- result: validateBuildJson(parsed),
681
+ result,
591
682
  })
592
683
  }
593
684
  reader.readAsText(file)
@@ -596,7 +687,20 @@ export function SettingsPanel({
596
687
  e.target.value = ''
597
688
  }
598
689
 
599
- const handleConfirmImport = (parsed: ParsedBuildJson) => {
690
+ const handleConfirmImport = async (file: ParsedBuildJson) => {
691
+ const targetProjectId = useViewer.getState().projectId
692
+ const artifactStore = getArtifactStore()
693
+ const { build: parsed, refusedIds, refusal } = await bringBuildArtifacts(file)
694
+ if (
695
+ targetProjectId !== useViewer.getState().projectId ||
696
+ artifactStore !== getArtifactStore()
697
+ )
698
+ return
699
+ if (refusal) {
700
+ useFloorplanMode
701
+ .getState()
702
+ .showNotice(refusedObjectsNotice(refusedIds.length, refusal, 'loaded'))
703
+ }
600
704
  const currentScene = useScene.getState()
601
705
  setScene(
602
706
  parsed.nodes as Parameters<typeof setScene>[0],
@@ -626,7 +730,7 @@ export function SettingsPanel({
626
730
  // old scene (or land on the empty intermediate `unloadScene` state).
627
731
  clearSceneHistory()
628
732
  resetSelection()
629
- setPhase('structure')
733
+ setPhase('building')
630
734
  selectDefaultBuildingAndLevel()
631
735
  }
632
736
 
@@ -688,6 +792,7 @@ export function SettingsPanel({
688
792
  setModelExportWarning(null)
689
793
  try {
690
794
  const artifact = await modelExport(format, {
795
+ projectName,
691
796
  onlyVisible: exportOnlyVisible,
692
797
  excludedNodeTypes,
693
798
  includedPresentationIds:
@@ -903,7 +1008,7 @@ export function SettingsPanel({
903
1008
  <legend className="px-1 font-medium text-sm">Include in file</legend>
904
1009
  <p className="text-muted-foreground text-xs">
905
1010
  Choose which procedural content is baked into model files. GLB and USDZ use the
906
- textured portable path; STL and OBJ remain geometry-only.
1011
+ textured portable path; STL, OBJ and IFC remain geometry-only.
907
1012
  </p>
908
1013
  {exportableNodeTypes.length > 0 ? (
909
1014
  <div className="space-y-2 pt-1">
@@ -1157,7 +1262,13 @@ export function SettingsPanel({
1157
1262
  />
1158
1263
  </SettingsSection>
1159
1264
 
1160
- <SettingsSection description="Sound levels and keyboard shortcuts." title="Preferences">
1265
+ <SettingsSection
1266
+ description="Sound levels, the construction animation and keyboard shortcuts."
1267
+ title="Preferences"
1268
+ >
1269
+ <ConstructionAnimationSetting />
1270
+ <AnimationSpeedSetting />
1271
+ {preferences}
1161
1272
  <div className="grid gap-2 @sm:grid-cols-2">
1162
1273
  <AudioSettingsDialog />
1163
1274
  <KeyboardShortcutsDialog />
@@ -1177,7 +1288,7 @@ export function SettingsPanel({
1177
1288
  </Button>
1178
1289
  </div>
1179
1290
  </DialogTrigger>
1180
- <DialogContent className="h-[80vh] max-w-[95vw] gap-0 overflow-hidden border-0 bg-[#1e1e1e] p-0 shadow-none sm:max-w-5xl">
1291
+ <DialogContent className="dark h-[80vh] max-w-[95vw] gap-0 overflow-hidden border-0 bg-[#1e1e1e] p-0 shadow-none sm:max-w-5xl">
1181
1292
  <DialogTitle className="sr-only">Scene Graph</DialogTitle>
1182
1293
  <div
1183
1294
  className="flex h-full min-h-0 w-full min-w-0 *:h-full *:w-full *:overflow-y-auto"
@@ -0,0 +1,20 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { InterfaceThemeRow, SettingsPanel } from '.'
4
+
5
+ const pressedLabels = (html: string) =>
6
+ [...html.matchAll(/aria-pressed="true"[^>]*>(?:<[^>]+>)*([^<]+)/g)].map((match) => match[1])
7
+
8
+ describe('Interface theme setting', () => {
9
+ test("the row shows the host's value pressed", () => {
10
+ const html = renderToStaticMarkup(<InterfaceThemeRow onChange={() => {}} value="light" />)
11
+ expect(html).toContain('Interface theme')
12
+ expect(pressedLabels(html)).toEqual(['Light'])
13
+ })
14
+
15
+ test('Settings shows the row a host passes, and none without it', () => {
16
+ const row = <InterfaceThemeRow onChange={() => {}} value="system" />
17
+ expect(renderToStaticMarkup(<SettingsPanel preferences={row} />)).toContain('Interface theme')
18
+ expect(renderToStaticMarkup(<SettingsPanel />)).not.toContain('Interface theme')
19
+ })
20
+ })
@@ -28,11 +28,11 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
28
28
  {
29
29
  title: 'Editor Navigation',
30
30
  shortcuts: [
31
- { keys: ['1'], action: 'Switch to Site phase' },
32
- { keys: ['2'], action: 'Switch to Structure phase' },
33
- { keys: ['3'], action: 'Switch to Furnish phase' },
34
- { keys: ['F'], action: 'Switch to Furnish layer' },
35
- { keys: ['Z'], action: 'Switch to Zones layer' },
31
+ { keys: ['1'], action: 'Switch to Site' },
32
+ { keys: ['2'], action: 'Switch to Building' },
33
+ { keys: ['\\'], action: 'Open or close the split view' },
34
+ { keys: ['F'], action: 'Open the items catalog with the item tool' },
35
+ { keys: ['Z'], action: 'Draw a zone' },
36
36
  {
37
37
  keys: ['Cmd/Ctrl', 'Arrow Up'],
38
38
  action: 'Select next level in the active building',