@pascal-app/editor 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -1,29 +1,40 @@
1
1
  import {
2
2
  clearSceneHistory,
3
+ DEFAULT_LEVEL_HEIGHT,
3
4
  emitter,
5
+ getArtifactStore,
6
+ getLevelDisplayName,
7
+ LevelNode,
8
+ type AnyNodeId,
9
+ type BuildingNode,
4
10
  isNodeKindEnabled,
5
11
  nodeRegistry,
12
+ type ParsedBuildJson,
6
13
  useRegistryVersion,
7
14
  useScene,
8
- type ParsedBuildJson,
9
15
  validateBuildJson,
10
16
  } from '@pascal-app/core'
11
17
  import { useViewer, viewerPresentationRegistry } from '@pascal-app/viewer'
12
18
  import { TreeView, VisualJson } from '@visual-json/react'
13
19
  import {
20
+ ArrowDown,
21
+ ArrowUp,
14
22
  Camera,
15
23
  Check,
16
- ChevronDown,
17
24
  Copy,
18
25
  Download,
26
+ Layers,
27
+ ListTree,
19
28
  Map as MapIcon,
20
29
  Save,
30
+ ChevronDown,
21
31
  Send,
22
32
  Trash2,
23
33
  Upload,
24
34
  } from 'lucide-react'
25
35
  import {
26
36
  type KeyboardEvent,
37
+ type ReactNode,
27
38
  type SyntheticEvent,
28
39
  useCallback,
29
40
  useEffect,
@@ -33,6 +44,7 @@ import {
33
44
  useState,
34
45
  useSyncExternalStore,
35
46
  } from 'react'
47
+ import { useShallow } from 'zustand/react/shallow'
36
48
  import {
37
49
  exportFloorplanPdf,
38
50
  type FloorplanExportScope,
@@ -52,9 +64,13 @@ import {
52
64
  } from './../../../../../components/ui/primitives/dialog'
53
65
  import { Input } from './../../../../../components/ui/primitives/input'
54
66
  import { Switch } from './../../../../../components/ui/primitives/switch'
67
+ import { cn } from './../../../../../lib/utils'
68
+ import { refusedObjectsNotice } from './../../../../../components/editor/group-actions'
69
+ import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection'
70
+ import { bringBuildArtifacts } from './../../../../../lib/scene-clipboard'
55
71
  import useEditor, { selectDefaultBuildingAndLevel } from './../../../../../store/use-editor'
56
- import { type SendToAppStep, useSendToApp } from './../../../../../store/use-send-to-app'
57
72
  import useFloorplanMode from './../../../../../store/use-floorplan-mode'
73
+ import { type SendToAppStep, useSendToApp } from './../../../../../store/use-send-to-app'
58
74
  import { AudioSettingsDialog } from './audio-settings-dialog'
59
75
  import { KeyboardShortcutsDialog } from './keyboard-shortcuts-dialog'
60
76
  import { LoadBuildDialog, type PendingImport } from './load-build-dialog'
@@ -88,6 +104,7 @@ const MODEL_EXPORT_FORMATS = [
88
104
  { format: 'usdz', label: 'USDZ' },
89
105
  { format: 'stl', label: 'STL' },
90
106
  { format: 'obj', label: 'OBJ' },
107
+ { format: 'ifc', label: 'IFC' },
91
108
  ] as const
92
109
 
93
110
  type ModelExportFormat = (typeof MODEL_EXPORT_FORMATS)[number]['format']
@@ -223,7 +240,69 @@ export interface ProjectVisibility {
223
240
  showGuidesPublic: boolean
224
241
  }
225
242
 
243
+ function SettingsSection({
244
+ children,
245
+ description,
246
+ title,
247
+ tone = 'default',
248
+ }: {
249
+ children: ReactNode
250
+ description?: string
251
+ title: string
252
+ tone?: 'default' | 'destructive'
253
+ }) {
254
+ const headingId = useId()
255
+ return (
256
+ <section aria-labelledby={headingId} className="space-y-3 py-5 first:pt-0 last:pb-0">
257
+ <div className="space-y-0.5">
258
+ <h3
259
+ className={cn(
260
+ 'font-semibold text-sm',
261
+ tone === 'destructive' ? 'text-destructive' : 'text-foreground',
262
+ )}
263
+ id={headingId}
264
+ >
265
+ {title}
266
+ </h3>
267
+ {description ? <p className="text-muted-foreground text-xs">{description}</p> : null}
268
+ </div>
269
+ {children}
270
+ </section>
271
+ )
272
+ }
273
+
274
+ function SettingsSubheading({ children }: { children: ReactNode }) {
275
+ return <h4 className="font-medium text-muted-foreground text-xs">{children}</h4>
276
+ }
277
+
278
+ function SettingsSwitchRow({
279
+ checked,
280
+ description,
281
+ label,
282
+ onCheckedChange,
283
+ }: {
284
+ checked: boolean
285
+ description: string
286
+ label: string
287
+ onCheckedChange: (checked: boolean) => void
288
+ }) {
289
+ const id = useId()
290
+ return (
291
+ <div className="flex items-center justify-between gap-4 py-1">
292
+ <div className="min-w-0 space-y-0.5">
293
+ <label className="block font-medium text-sm" htmlFor={id}>
294
+ {label}
295
+ </label>
296
+ <p className="text-muted-foreground text-xs">{description}</p>
297
+ </div>
298
+ <Switch checked={checked} id={id} onCheckedChange={onCheckedChange} />
299
+ </div>
300
+ )
301
+ }
302
+
226
303
  export interface SettingsPanelProps {
304
+ /** Merged onto the scrolling root so hosts can restyle padding (e.g. inside a dialog). */
305
+ className?: string
227
306
  projectId?: string
228
307
  /** Shown as the scene name in apps the scene is sent to (Blender collection name). */
229
308
  projectName?: string
@@ -234,7 +313,124 @@ export interface SettingsPanelProps {
234
313
  ) => Promise<void>
235
314
  }
236
315
 
316
+ function FloorSettingsSection() {
317
+ const selectedBuildingId = useViewer((state) => state.selection.buildingId)
318
+ const selectedLevelId = useViewer((state) => state.selection.levelId)
319
+ const setSelection = useViewer((state) => state.setSelection)
320
+ const createNode = useScene((state) => state.createNode)
321
+ const building = useScene((state) => {
322
+ const selected = selectedBuildingId ? state.nodes[selectedBuildingId] : undefined
323
+ if (selected?.type === 'building') return selected as BuildingNode
324
+
325
+ const site = state.rootNodeIds
326
+ .map((nodeId) => state.nodes[nodeId])
327
+ .find((node) => node?.type === 'site')
328
+ if (site?.type !== 'site') return null
329
+
330
+ return (
331
+ site.children
332
+ .map((childId) => state.nodes[childId as AnyNodeId])
333
+ .find((node): node is BuildingNode => node?.type === 'building') ?? null
334
+ )
335
+ })
336
+ const levels = useScene(
337
+ useShallow((state) => {
338
+ if (!building) return []
339
+
340
+ return building.children
341
+ .map((childId) => state.nodes[childId as AnyNodeId])
342
+ .filter((node): node is LevelNode => node?.type === 'level')
343
+ .sort((a, b) => b.level - a.level)
344
+ }),
345
+ )
346
+
347
+ if (!building) return null
348
+
349
+ const addLevel = (level: number) => {
350
+ const newLevel = LevelNode.parse({
351
+ level,
352
+ height: DEFAULT_LEVEL_HEIGHT,
353
+ children: [],
354
+ parentId: building.id,
355
+ })
356
+ createNode(newLevel, building.id)
357
+ setSelection({ buildingId: building.id, levelId: newLevel.id })
358
+ }
359
+
360
+ const addFloor = () =>
361
+ addLevel(levels.length === 0 ? 0 : Math.max(0, ...levels.map((level) => level.level)) + 1)
362
+ const addBasement = () => addLevel(Math.min(0, ...levels.map((level) => level.level)) - 1)
363
+
364
+ return (
365
+ <div className="space-y-3">
366
+ <label className="font-medium text-muted-foreground text-xs uppercase">Floors</label>
367
+ <div className="grid grid-cols-2 gap-2">
368
+ <Button className="justify-start" onClick={addFloor} size="sm" type="button" variant="outline">
369
+ <ArrowUp className="size-3.5" />
370
+ Add floor
371
+ </Button>
372
+ <Button
373
+ className="justify-start"
374
+ onClick={addBasement}
375
+ size="sm"
376
+ type="button"
377
+ variant="outline"
378
+ >
379
+ <ArrowDown className="size-3.5" />
380
+ Add basement
381
+ </Button>
382
+ </div>
383
+
384
+ <div className="space-y-1 rounded-md border p-1">
385
+ {levels.length === 0 ? (
386
+ <div className="px-2 py-2 text-muted-foreground text-xs">No floors yet</div>
387
+ ) : (
388
+ levels.map((level) => {
389
+ const isSelected = level.id === selectedLevelId
390
+ const canDelete = level.level !== 0
391
+
392
+ return (
393
+ <div className="flex items-center gap-1" key={level.id}>
394
+ <button
395
+ className={cn(
396
+ 'flex min-w-0 flex-1 items-center gap-2 rounded px-2 py-1.5 text-left text-sm transition-colors',
397
+ isSelected
398
+ ? 'bg-accent text-foreground'
399
+ : 'text-muted-foreground hover:bg-accent/60 hover:text-foreground',
400
+ )}
401
+ onClick={() => setSelection({ buildingId: building.id, levelId: level.id })}
402
+ type="button"
403
+ >
404
+ <Layers className="size-3.5 shrink-0" />
405
+ <span className="truncate">{getLevelDisplayName(level)}</span>
406
+ <span className="ml-auto shrink-0 text-muted-foreground text-xs">
407
+ {level.level === 0 ? '0' : level.level > 0 ? `+${level.level}` : level.level}
408
+ </span>
409
+ </button>
410
+ <button
411
+ aria-label={`Remove ${getLevelDisplayName(level)}`}
412
+ className="flex size-7 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive disabled:cursor-not-allowed disabled:opacity-30"
413
+ disabled={!canDelete}
414
+ onClick={() => deleteLevelWithFallbackSelection(level.id)}
415
+ title={canDelete ? 'Remove floor' : 'The ground floor cannot be removed'}
416
+ type="button"
417
+ >
418
+ <Trash2 className="size-3.5" />
419
+ </button>
420
+ </div>
421
+ )
422
+ })
423
+ )}
424
+ </div>
425
+ <p className="text-muted-foreground text-xs">
426
+ Select a floor to make it active. The ground floor is always kept as the building datum.
427
+ </p>
428
+ </div>
429
+ )
430
+ }
431
+
237
432
  export function SettingsPanel({
433
+ className,
238
434
  projectId,
239
435
  projectName,
240
436
  projectVisibility,
@@ -348,7 +544,16 @@ export function SettingsPanel({
348
544
  // Materials ride along: nodes reference them by `scene:<id>` slot
349
545
  // refs, so a save without the table produces a file whose custom
350
546
  // finishes revert to defaults on the very Load Build path below.
351
- const sceneData = { nodes, rootNodeIds, installedPlugins, materials, collections }
547
+ // The project rides along too: a scripted object's artifacts live there, and
548
+ // loading the file into another project copies them over as a paste does.
549
+ const sceneData = {
550
+ nodes,
551
+ rootNodeIds,
552
+ installedPlugins,
553
+ materials,
554
+ collections,
555
+ projectId: useViewer.getState().projectId,
556
+ }
352
557
  const json = JSON.stringify(sceneData, null, 2)
353
558
  const blob = new Blob([json], { type: 'application/json' })
354
559
  const url = URL.createObjectURL(blob)
@@ -404,7 +609,20 @@ export function SettingsPanel({
404
609
  e.target.value = ''
405
610
  }
406
611
 
407
- const handleConfirmImport = (parsed: ParsedBuildJson) => {
612
+ const handleConfirmImport = async (file: ParsedBuildJson) => {
613
+ const targetProjectId = useViewer.getState().projectId
614
+ const artifactStore = getArtifactStore()
615
+ const { build: parsed, refusedIds, refusal } = await bringBuildArtifacts(file)
616
+ if (
617
+ targetProjectId !== useViewer.getState().projectId ||
618
+ artifactStore !== getArtifactStore()
619
+ )
620
+ return
621
+ if (refusal) {
622
+ useFloorplanMode
623
+ .getState()
624
+ .showNotice(refusedObjectsNotice(refusedIds.length, refusal, 'loaded'))
625
+ }
408
626
  const currentScene = useScene.getState()
409
627
  setScene(
410
628
  parsed.nodes as Parameters<typeof setScene>[0],
@@ -434,7 +652,7 @@ export function SettingsPanel({
434
652
  // old scene (or land on the empty intermediate `unloadScene` state).
435
653
  clearSceneHistory()
436
654
  resetSelection()
437
- setPhase('structure')
655
+ setPhase('building')
438
656
  selectDefaultBuildingAndLevel()
439
657
  }
440
658
 
@@ -496,6 +714,7 @@ export function SettingsPanel({
496
714
  setModelExportWarning(null)
497
715
  try {
498
716
  const artifact = await modelExport(format, {
717
+ projectName,
499
718
  onlyVisible: exportOnlyVisible,
500
719
  excludedNodeTypes,
501
720
  includedPresentationIds:
@@ -591,409 +810,433 @@ export function SettingsPanel({
591
810
  }
592
811
 
593
812
  return (
594
- <div className="subtle-scrollbar min-h-0 flex-1 space-y-6 overflow-x-hidden overflow-y-auto overscroll-contain p-3">
595
- {projectId && (
596
- <div className="space-y-2">
597
- <label className="font-medium text-muted-foreground text-xs uppercase">Project</label>
598
- <div className="font-medium text-sm">Project ID</div>
599
- <div className="flex items-center gap-2">
600
- <Input
601
- aria-label="Project ID"
602
- className="font-mono text-xs"
603
- readOnly
604
- value={projectId}
605
- />
606
- <Button
607
- aria-label={projectIdCopyState === 'copied' ? 'Project ID copied' : 'Copy project ID'}
608
- className="rounded-full"
609
- onClick={() => void handleCopyProjectId()}
610
- size="sm"
611
- type="button"
612
- variant="outline"
613
- >
614
- {projectIdCopyState === 'copied' ? (
615
- <Check className="size-3.5" />
616
- ) : (
617
- <Copy className="size-3.5" />
618
- )}
619
- {projectIdCopyState === 'copied'
620
- ? 'Copied'
621
- : projectIdCopyState === 'error'
622
- ? 'Try again'
623
- : 'Copy'}
624
- </Button>
625
- </div>
626
- </div>
813
+ <div
814
+ className={cn(
815
+ 'subtle-scrollbar @container min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-contain p-3',
816
+ className,
627
817
  )}
628
-
629
- {/* Visibility Section (only for cloud projects) */}
630
- {projectId && !isLocalProject && (
631
- <div className="space-y-3">
632
- <label className="font-medium text-muted-foreground text-xs uppercase">Visibility</label>
633
- <div className="flex items-center justify-between">
634
- <div>
635
- <div className="font-medium text-sm">Public</div>
636
- <div className="text-muted-foreground text-xs">
637
- {projectVisibility?.isPrivate ? 'Only you' : 'Anyone'} can view
638
- </div>
818
+ >
819
+ <div className="divide-y divide-border/60">
820
+ <section className="space-y-3 py-5 first:pt-0 last:pb-0">
821
+ <FloorSettingsSection />
822
+ </section>
823
+ {projectId && (
824
+ <SettingsSection
825
+ description="Reference this project from the API, MCP tools, or support."
826
+ title="Project"
827
+ >
828
+ <div className="flex items-center gap-2">
829
+ <Input
830
+ aria-label="Project ID"
831
+ className="font-mono text-xs"
832
+ readOnly
833
+ value={projectId}
834
+ />
835
+ <Button
836
+ aria-label={
837
+ projectIdCopyState === 'copied' ? 'Project ID copied' : 'Copy project ID'
838
+ }
839
+ className="rounded-full"
840
+ onClick={() => void handleCopyProjectId()}
841
+ size="sm"
842
+ type="button"
843
+ variant="outline"
844
+ >
845
+ {projectIdCopyState === 'copied' ? (
846
+ <Check className="size-3.5" />
847
+ ) : (
848
+ <Copy className="size-3.5" />
849
+ )}
850
+ {projectIdCopyState === 'copied'
851
+ ? 'Copied'
852
+ : projectIdCopyState === 'error'
853
+ ? 'Try again'
854
+ : 'Copy'}
855
+ </Button>
639
856
  </div>
640
- <Switch
641
- aria-label="Make project public"
857
+ </SettingsSection>
858
+ )}
859
+
860
+ {/* Sharing (only for cloud projects) */}
861
+ {projectId && !isLocalProject && (
862
+ <SettingsSection description="Control who can open this project." title="Sharing">
863
+ <SettingsSwitchRow
642
864
  checked={!(projectVisibility?.isPrivate ?? false)}
865
+ description={
866
+ projectVisibility?.isPrivate
867
+ ? 'Only you can view it.'
868
+ : 'Anyone with the link can view it.'
869
+ }
870
+ label="Public project"
643
871
  onCheckedChange={(checked) => handleVisibilityChange('isPrivate', !checked)}
644
872
  />
645
- </div>
646
- <div className="flex items-center justify-between">
647
- <div>
648
- <div className="font-medium text-sm">Show 3D Scans</div>
649
- <div className="text-muted-foreground text-xs">Visible to public viewers</div>
650
- </div>
651
- <Switch
652
- aria-label="Show 3D scans to public viewers"
873
+ <SettingsSwitchRow
653
874
  checked={projectVisibility?.showScansPublic ?? true}
875
+ description="Visible in the public viewer."
876
+ label="Show 3D scans"
654
877
  onCheckedChange={(checked) => handleVisibilityChange('showScansPublic', checked)}
655
878
  />
656
- </div>
657
- <div className="flex items-center justify-between">
658
- <div>
659
- <div className="font-medium text-sm">Show Floorplans</div>
660
- <div className="text-muted-foreground text-xs">Visible to public viewers</div>
661
- </div>
662
- <Switch
663
- aria-label="Show floorplans to public viewers"
879
+ <SettingsSwitchRow
664
880
  checked={projectVisibility?.showGuidesPublic ?? true}
881
+ description="Visible in the public viewer."
882
+ label="Show floor plans"
665
883
  onCheckedChange={(checked) => handleVisibilityChange('showGuidesPublic', checked)}
666
884
  />
667
- </div>
668
- <div className="flex items-center justify-between">
669
- <div>
670
- <div className="font-medium text-sm">Shadows</div>
671
- <div className="text-muted-foreground text-xs">Cast shadows from lights</div>
672
- </div>
673
- <Switch
674
- aria-label="Enable shadows"
675
- checked={shadows}
676
- onCheckedChange={(checked) => useViewer.getState().setShadows(checked)}
677
- />
678
- </div>
679
- </div>
680
- )}
681
-
682
- {/* Export Section */}
683
- <div className="space-y-4">
684
- <label className="font-medium text-muted-foreground text-xs uppercase">Export</label>
685
-
686
- <div className="space-y-2">
687
- <div className="font-medium text-muted-foreground text-xs">3D model</div>
688
- <details
689
- className="group"
690
- onKeyDownCapture={(event) => {
691
- // Keep Space available to the disclosure and switches, not canvas panning.
692
- if (event.code === 'Space') event.stopPropagation()
693
- }}
694
- >
695
- <summary className="flex min-h-10 cursor-pointer list-none items-center justify-between rounded-md border px-3 text-sm font-medium focus-visible:outline-2 focus-visible:outline-ring">
696
- Export options
697
- <ChevronDown aria-hidden="true" className="size-4 group-open:rotate-180" />
698
- </summary>
699
- <div className="space-y-3 pt-3">
700
- <div className="flex items-center justify-between gap-4 rounded-md border p-3">
701
- <div className="min-w-0">
702
- <label className="font-medium text-sm" htmlFor={visibleOnlySwitchId}>
703
- Visible nodes only
704
- </label>
705
- <div className="text-muted-foreground text-xs">
706
- Exclude hidden furniture and other hidden scene nodes
885
+ </SettingsSection>
886
+ )}
887
+
888
+ <SettingsSection description="Rendering options for this editor session." title="Viewer">
889
+ <SettingsSwitchRow
890
+ checked={shadows}
891
+ description="Cast shadows from lights."
892
+ label="Shadows"
893
+ onCheckedChange={(checked) => useViewer.getState().setShadows(checked)}
894
+ />
895
+ </SettingsSection>
896
+
897
+ <SettingsSection description="Download the scene or hand it to another app." title="Export">
898
+ <div className="space-y-3">
899
+ <SettingsSubheading>3D model</SettingsSubheading>
900
+ <details
901
+ className="group"
902
+ onKeyDownCapture={(event) => {
903
+ // Keep Space available to the disclosure and switches, not canvas panning.
904
+ if (event.code === 'Space') event.stopPropagation()
905
+ }}
906
+ >
907
+ <summary className="flex min-h-10 cursor-pointer list-none items-center justify-between rounded-md border px-3 font-medium text-sm focus-visible:outline-2 focus-visible:outline-ring">
908
+ Export options
909
+ <ChevronDown aria-hidden="true" className="size-4 group-open:rotate-180" />
910
+ </summary>
911
+ <div className="space-y-3 pt-3">
912
+ <div className="flex items-center justify-between gap-4 rounded-md border p-3">
913
+ <div className="min-w-0">
914
+ <label className="font-medium text-sm" htmlFor={visibleOnlySwitchId}>
915
+ Visible nodes only
916
+ </label>
917
+ <div className="text-muted-foreground text-xs">
918
+ Exclude hidden furniture and other hidden scene nodes
919
+ </div>
707
920
  </div>
921
+ <Switch
922
+ aria-label="Export visible nodes only"
923
+ checked={exportOnlyVisible}
924
+ id={visibleOnlySwitchId}
925
+ onCheckedChange={setExportOnlyVisible}
926
+ />
708
927
  </div>
709
- <Switch
710
- aria-label="Export visible nodes only"
711
- checked={exportOnlyVisible}
712
- id={visibleOnlySwitchId}
713
- onCheckedChange={setExportOnlyVisible}
714
- />
715
- </div>
716
928
 
717
- <fieldset className="space-y-2 rounded-md border p-3">
718
- <legend className="px-1 font-medium text-sm">Include in file</legend>
719
- <p className="text-muted-foreground text-xs">
720
- Choose which procedural content is baked into model files. GLB and USDZ use the
721
- textured portable path; STL and OBJ remain geometry-only.
722
- </p>
723
- {exportableNodeTypes.length > 0 ? (
724
- <div className="space-y-2 pt-1">
725
- {exportableNodeTypes.map(({ type, label, supportsGeometryOnly }, index) => {
726
- const switchId = `${includeNodeTypeIdPrefix}-${index}`
727
- return (
728
- <div className="flex items-center justify-between gap-4" key={type}>
729
- <label className="min-w-0 font-medium text-sm" htmlFor={switchId}>
730
- {label}
731
- {!supportsGeometryOnly && (
732
- <span className="text-muted-foreground text-xs"> (GLB/USDZ only)</span>
733
- )}
734
- </label>
735
- <Switch
736
- aria-label={`Include ${label} in model files`}
737
- checked={!excludedNodeTypes.includes(type)}
738
- id={switchId}
739
- onCheckedChange={(included) => handleNodeTypeInclusion(type, included)}
740
- />
741
- </div>
742
- )
743
- })}
744
- </div>
745
- ) : (
929
+ <fieldset className="space-y-2 rounded-md border p-3">
930
+ <legend className="px-1 font-medium text-sm">Include in file</legend>
746
931
  <p className="text-muted-foreground text-xs">
747
- No optional procedural content is present.
932
+ Choose which procedural content is baked into model files. GLB and USDZ use the
933
+ textured portable path; STL, OBJ and IFC remain geometry-only.
748
934
  </p>
749
- )}
750
- <p className="text-muted-foreground text-xs">
751
- Viewer surroundings are excluded unless selected separately below.
752
- </p>
753
- </fieldset>
754
- {exportablePresentations.length > 0 ? (
755
- <fieldset className="space-y-2 rounded-md border p-3">
756
- <legend className="px-1 font-medium text-sm">Viewer surroundings</legend>
935
+ {exportableNodeTypes.length > 0 ? (
936
+ <div className="space-y-2 pt-1">
937
+ {exportableNodeTypes.map(({ type, label, supportsGeometryOnly }, index) => {
938
+ const switchId = `${includeNodeTypeIdPrefix}-${index}`
939
+ return (
940
+ <div className="flex items-center justify-between gap-4" key={type}>
941
+ <label className="min-w-0 font-medium text-sm" htmlFor={switchId}>
942
+ {label}
943
+ {!supportsGeometryOnly && (
944
+ <span className="text-muted-foreground text-xs">
945
+ {' '}
946
+ (GLB/USDZ only)
947
+ </span>
948
+ )}
949
+ </label>
950
+ <Switch
951
+ aria-label={`Include ${label} in model files`}
952
+ checked={!excludedNodeTypes.includes(type)}
953
+ id={switchId}
954
+ onCheckedChange={(included) =>
955
+ handleNodeTypeInclusion(type, included)
956
+ }
957
+ />
958
+ </div>
959
+ )
960
+ })}
961
+ </div>
962
+ ) : (
963
+ <p className="text-muted-foreground text-xs">
964
+ No optional procedural content is present.
965
+ </p>
966
+ )}
757
967
  <p className="text-muted-foreground text-xs">
758
- Optional static surroundings are included only in GLB and USDZ.
968
+ Viewer surroundings are excluded unless selected separately below.
759
969
  </p>
760
- <div className="space-y-2 pt-1">
761
- {exportablePresentations.map((contribution, index) => {
762
- const switchId = `${includePresentationIdPrefix}-${index}`
763
- const label = contribution.staticExport!.label
764
- return (
765
- <div
766
- className="flex items-center justify-between gap-4"
767
- key={contribution.id}
768
- >
769
- <label className="min-w-0 font-medium text-sm" htmlFor={switchId}>
770
- {label}
771
- </label>
772
- <Switch
773
- aria-label={`Include ${label} in GLB and USDZ`}
774
- checked={includedPresentationIds.includes(contribution.id)}
775
- id={switchId}
776
- onCheckedChange={(included) =>
777
- handlePresentationInclusion(contribution.id, included)
778
- }
779
- />
780
- </div>
781
- )
782
- })}
783
- </div>
784
970
  </fieldset>
785
- ) : null}
971
+ {exportablePresentations.length > 0 ? (
972
+ <fieldset className="space-y-2 rounded-md border p-3">
973
+ <legend className="px-1 font-medium text-sm">Viewer surroundings</legend>
974
+ <p className="text-muted-foreground text-xs">
975
+ Optional static surroundings are included only in GLB and USDZ.
976
+ </p>
977
+ <div className="space-y-2 pt-1">
978
+ {exportablePresentations.map((contribution, index) => {
979
+ const switchId = `${includePresentationIdPrefix}-${index}`
980
+ const label = contribution.staticExport!.label
981
+ return (
982
+ <div
983
+ className="flex items-center justify-between gap-4"
984
+ key={contribution.id}
985
+ >
986
+ <label className="min-w-0 font-medium text-sm" htmlFor={switchId}>
987
+ {label}
988
+ </label>
989
+ <Switch
990
+ aria-label={`Include ${label} in GLB and USDZ`}
991
+ checked={includedPresentationIds.includes(contribution.id)}
992
+ id={switchId}
993
+ onCheckedChange={(included) =>
994
+ handlePresentationInclusion(contribution.id, included)
995
+ }
996
+ />
997
+ </div>
998
+ )
999
+ })}
1000
+ </div>
1001
+ </fieldset>
1002
+ ) : null}
1003
+ </div>
1004
+ </details>
1005
+
1006
+ <div className="grid gap-2 @sm:grid-cols-2">
1007
+ {MODEL_EXPORT_FORMATS.map(({ format, label }) => {
1008
+ const isActive = activeModelExport === format
1009
+ return (
1010
+ <Button
1011
+ aria-busy={isActive}
1012
+ className="w-full justify-start gap-2"
1013
+ disabled={
1014
+ activeModelExport !== null || sendToBlenderStep !== null || !modelExport
1015
+ }
1016
+ key={format}
1017
+ onClick={() => void handleModelExport(format, label)}
1018
+ variant="outline"
1019
+ >
1020
+ <Download aria-hidden="true" className="size-4" />
1021
+ {isActive ? `Exporting ${label}…` : `Export ${label}`}
1022
+ </Button>
1023
+ )
1024
+ })}
786
1025
  </div>
787
- </details>
788
1026
 
789
- {MODEL_EXPORT_FORMATS.map(({ format, label }) => {
790
- const isActive = activeModelExport === format
791
- return (
1027
+ <div className="space-y-2">
792
1028
  <Button
793
- aria-busy={isActive}
1029
+ aria-busy={sendToBlenderStep !== null}
794
1030
  className="w-full justify-start gap-2"
795
1031
  disabled={activeModelExport !== null || sendToBlenderStep !== null || !modelExport}
796
- key={format}
797
- onClick={() => void handleModelExport(format, label)}
1032
+ onClick={() => void handleSendToBlender()}
798
1033
  variant="outline"
799
1034
  >
800
- <Download aria-hidden="true" className="size-4" />
801
- {isActive ? `Exporting ${label}…` : `Export ${label}`}
1035
+ <Send aria-hidden="true" className="size-4" />
1036
+ {sendToBlenderStep
1037
+ ? SEND_TO_BLENDER_STEP_LABEL[sendToBlenderStep]
1038
+ : 'Send to Blender'}
802
1039
  </Button>
803
- )
804
- })}
805
-
806
- <Button
807
- aria-busy={sendToBlenderStep !== null}
808
- className="w-full justify-start gap-2"
809
- disabled={activeModelExport !== null || sendToBlenderStep !== null || !modelExport}
810
- onClick={() => void handleSendToBlender()}
811
- variant="outline"
812
- >
813
- <Send aria-hidden="true" className="size-4" />
814
- {sendToBlenderStep ? SEND_TO_BLENDER_STEP_LABEL[sendToBlenderStep] : 'Send to Blender'}
815
- </Button>
816
- <p className="text-muted-foreground text-xs">
817
- Needs the{' '}
818
- <a
819
- className="underline underline-offset-2"
820
- href={BLENDER_ADDON_URL}
821
- rel="noreferrer"
822
- target="_blank"
823
- >
824
- Pascal add-on for Blender
825
- </a>{' '}
826
- running in an open Blender.
827
- </p>
828
- {sendToBlenderMessage ? (
829
- <p
830
- className={
831
- sendToBlenderMessage.tone === 'error'
832
- ? 'text-destructive text-xs'
833
- : 'text-foreground text-xs'
834
- }
835
- role={sendToBlenderMessage.tone === 'error' ? 'alert' : 'status'}
836
- >
837
- {sendToBlenderMessage.text}
838
- </p>
839
- ) : null}
840
-
841
- {activeModelExport ? (
842
- <p className="text-muted-foreground text-xs" role="status">
843
- Preparing {activeModelExport.toUpperCase()} file…
844
- </p>
845
- ) : null}
846
- {modelExportError ? (
847
- <p className="text-destructive text-xs" role="alert">
848
- {modelExportError}
849
- </p>
850
- ) : null}
851
- {modelExportWarning ? (
852
- <p className="text-foreground text-xs" role="status">
853
- Warning: {modelExportWarning}
854
- </p>
855
- ) : null}
856
-
857
- <PrintExportButton onlyVisible={exportOnlyVisible} />
858
- </div>
859
-
860
- <div className="space-y-2">
861
- <div className="flex items-center justify-between font-medium text-muted-foreground text-xs">
862
- <span>Floor plan</span>
863
- <span>{floorplanMode === 'default' ? 'Default mode' : 'Expert mode'}</span>
1040
+ <p className="text-muted-foreground text-xs">
1041
+ Needs the{' '}
1042
+ <a
1043
+ className="underline underline-offset-2"
1044
+ href={BLENDER_ADDON_URL}
1045
+ rel="noreferrer"
1046
+ target="_blank"
1047
+ >
1048
+ Pascal add-on for Blender
1049
+ </a>{' '}
1050
+ running in an open Blender.
1051
+ </p>
1052
+ </div>
1053
+ {sendToBlenderMessage ? (
1054
+ <p
1055
+ className={
1056
+ sendToBlenderMessage.tone === 'error'
1057
+ ? 'text-destructive text-xs'
1058
+ : 'text-foreground text-xs'
1059
+ }
1060
+ role={sendToBlenderMessage.tone === 'error' ? 'alert' : 'status'}
1061
+ >
1062
+ {sendToBlenderMessage.text}
1063
+ </p>
1064
+ ) : null}
1065
+
1066
+ {activeModelExport ? (
1067
+ <p className="text-muted-foreground text-xs" role="status">
1068
+ Preparing {activeModelExport.toUpperCase()} file…
1069
+ </p>
1070
+ ) : null}
1071
+ {modelExportError ? (
1072
+ <p className="text-destructive text-xs" role="alert">
1073
+ {modelExportError}
1074
+ </p>
1075
+ ) : null}
1076
+ {modelExportWarning ? (
1077
+ <p className="text-foreground text-xs" role="status">
1078
+ Warning: {modelExportWarning}
1079
+ </p>
1080
+ ) : null}
1081
+
1082
+ <div className="grid gap-2 @sm:grid-cols-2">
1083
+ <PrintExportButton onlyVisible={exportOnlyVisible} />
1084
+ </div>
864
1085
  </div>
865
- <Button
866
- aria-busy={activeFloorplanExport === 'full'}
867
- className="w-full justify-start gap-2"
868
- disabled={activeFloorplanExport !== null}
869
- onClick={() => void handleFloorplanExport('full')}
870
- variant="outline"
871
- >
872
- <MapIcon className="size-4" />
873
- Full floor plan
874
- </Button>
875
- <Button
876
- aria-busy={activeFloorplanExport === 'structure'}
877
- className="w-full justify-start gap-2"
878
- disabled={activeFloorplanExport !== null}
879
- onClick={() => void handleFloorplanExport('structure')}
880
- variant="outline"
881
- >
882
- <MapIcon className="size-4" />
883
- Structure only
884
- </Button>
885
- {activeFloorplanExport ? (
886
- <p className="text-muted-foreground text-xs" role="status">
887
- Preparing floor-plan PDF…
888
- </p>
889
- ) : null}
890
- {floorplanExportError ? (
891
- <p className="break-words text-destructive text-xs" role="alert">
892
- {floorplanExportError}
893
- </p>
894
- ) : null}
895
- </div>
896
- </div>
897
-
898
- {/* Thumbnail Section (only for cloud projects) */}
899
- {projectId && !isLocalProject && (
900
- <div className="space-y-2">
901
- <label className="font-medium text-muted-foreground text-xs uppercase">Thumbnail</label>
902
- <Button
903
- className="w-full justify-start gap-2"
904
- disabled={isGeneratingThumbnail}
905
- onClick={handleGenerateThumbnail}
906
- variant="outline"
907
- >
908
- <Camera className="size-4" />
909
- {isGeneratingThumbnail ? 'Generating...' : 'Generate Thumbnail'}
910
- </Button>
911
- </div>
912
- )}
913
1086
 
914
- {/* Save/Load Section */}
915
- <div className="space-y-2">
916
- <label className="font-medium text-muted-foreground text-xs uppercase">Save & Load</label>
1087
+ <div className="space-y-3 pt-2">
1088
+ <div className="flex items-center justify-between">
1089
+ <SettingsSubheading>Floor plan</SettingsSubheading>
1090
+ <span className="text-muted-foreground text-xs">
1091
+ {floorplanMode === 'default' ? 'Default mode' : 'Expert mode'}
1092
+ </span>
1093
+ </div>
1094
+ <div className="grid gap-2 @sm:grid-cols-2">
1095
+ <Button
1096
+ aria-busy={activeFloorplanExport === 'full'}
1097
+ className="w-full justify-start gap-2"
1098
+ disabled={activeFloorplanExport !== null}
1099
+ onClick={() => void handleFloorplanExport('full')}
1100
+ variant="outline"
1101
+ >
1102
+ <MapIcon className="size-4" />
1103
+ Full floor plan
1104
+ </Button>
1105
+ <Button
1106
+ aria-busy={activeFloorplanExport === 'structure'}
1107
+ className="w-full justify-start gap-2"
1108
+ disabled={activeFloorplanExport !== null}
1109
+ onClick={() => void handleFloorplanExport('structure')}
1110
+ variant="outline"
1111
+ >
1112
+ <MapIcon className="size-4" />
1113
+ Structure only
1114
+ </Button>
1115
+ </div>
1116
+ {activeFloorplanExport ? (
1117
+ <p className="text-muted-foreground text-xs" role="status">
1118
+ Preparing floor-plan PDF…
1119
+ </p>
1120
+ ) : null}
1121
+ {floorplanExportError ? (
1122
+ <p className="break-words text-destructive text-xs" role="alert">
1123
+ {floorplanExportError}
1124
+ </p>
1125
+ ) : null}
1126
+ </div>
1127
+ </SettingsSection>
917
1128
 
918
- <Button className="w-full justify-start gap-2" onClick={handleSaveBuild} variant="outline">
919
- <Save className="size-4" />
920
- Save Build
921
- </Button>
1129
+ {/* Thumbnail (only for cloud projects) */}
1130
+ {projectId && !isLocalProject && (
1131
+ <SettingsSection
1132
+ description="Refresh the preview shown in your project list."
1133
+ title="Thumbnail"
1134
+ >
1135
+ <div className="grid gap-2 @sm:grid-cols-2">
1136
+ <Button
1137
+ className="w-full justify-start gap-2"
1138
+ disabled={isGeneratingThumbnail}
1139
+ onClick={handleGenerateThumbnail}
1140
+ variant="outline"
1141
+ >
1142
+ <Camera className="size-4" />
1143
+ {isGeneratingThumbnail ? 'Generating…' : 'Generate thumbnail'}
1144
+ </Button>
1145
+ </div>
1146
+ </SettingsSection>
1147
+ )}
922
1148
 
923
- <Button
924
- className="w-full justify-start gap-2"
925
- onClick={() => fileInputRef.current?.click()}
926
- variant="outline"
1149
+ <SettingsSection
1150
+ description="Keep a JSON copy of the scene, or restore one."
1151
+ title="Save and load"
927
1152
  >
928
- <Upload className="size-4" />
929
- Load Build
930
- </Button>
931
-
932
- <input
933
- accept="application/json"
934
- className="hidden"
935
- onChange={handleFileLoad}
936
- ref={fileInputRef}
937
- type="file"
938
- />
939
-
940
- <LoadBuildDialog
941
- onCancel={() => setPendingImport(null)}
942
- onConfirm={handleConfirmImport}
943
- pending={pendingImport}
944
- />
945
- </div>
946
-
947
- {/* Audio Section */}
948
- <div className="space-y-2">
949
- <label className="font-medium text-muted-foreground text-xs uppercase">Audio</label>
950
- <AudioSettingsDialog />
951
- </div>
952
-
953
- {/* Keyboard Section */}
954
- <div className="space-y-2">
955
- <label className="font-medium text-muted-foreground text-xs uppercase">Keyboard</label>
956
- <KeyboardShortcutsDialog />
957
- </div>
958
-
959
- {/* Scene Graph */}
960
- <div className="space-y-1">
961
- <label className="font-medium text-muted-foreground text-xs uppercase">Scene Graph</label>
962
- <Dialog>
963
- <DialogTrigger asChild>
964
- <Button className="h-auto justify-start p-0 text-sm" variant="link">
965
- Explore scene graph
1153
+ <div className="grid gap-2 @sm:grid-cols-2">
1154
+ <Button
1155
+ className="w-full justify-start gap-2"
1156
+ onClick={handleSaveBuild}
1157
+ variant="outline"
1158
+ >
1159
+ <Save className="size-4" />
1160
+ Save build
966
1161
  </Button>
967
- </DialogTrigger>
968
- <DialogContent className="h-[80vh] max-w-[95vw] gap-0 overflow-hidden border-0 bg-[#1e1e1e] p-0 shadow-none sm:max-w-5xl">
969
- <DialogTitle className="sr-only">Scene Graph</DialogTitle>
970
- <div
971
- className="flex h-full min-h-0 w-full min-w-0 *:h-full *:w-full *:overflow-y-auto"
972
- onContextMenuCapture={blockSceneGraphMutations}
973
- onDragStartCapture={blockSceneGraphMutations}
974
- onDropCapture={blockSceneGraphMutations}
975
- onKeyDownCapture={blockSceneGraphDeletion}
1162
+ <Button
1163
+ className="w-full justify-start gap-2"
1164
+ onClick={() => fileInputRef.current?.click()}
1165
+ variant="outline"
976
1166
  >
977
- <VisualJson value={sceneGraphValue}>
978
- <TreeView showCounts />
979
- </VisualJson>
980
- </div>
981
- </DialogContent>
982
- </Dialog>
983
- </div>
1167
+ <Upload className="size-4" />
1168
+ Load build
1169
+ </Button>
1170
+ </div>
984
1171
 
985
- {/* Danger Zone */}
986
- <div className="space-y-2">
987
- <label className="font-medium text-destructive text-xs uppercase">Danger Zone</label>
1172
+ <input
1173
+ accept="application/json"
1174
+ className="hidden"
1175
+ onChange={handleFileLoad}
1176
+ ref={fileInputRef}
1177
+ type="file"
1178
+ />
1179
+
1180
+ <LoadBuildDialog
1181
+ onCancel={() => setPendingImport(null)}
1182
+ onConfirm={handleConfirmImport}
1183
+ pending={pendingImport}
1184
+ />
1185
+ </SettingsSection>
1186
+
1187
+ <SettingsSection description="Sound levels and keyboard shortcuts." title="Preferences">
1188
+ <div className="grid gap-2 @sm:grid-cols-2">
1189
+ <AudioSettingsDialog />
1190
+ <KeyboardShortcutsDialog />
1191
+ </div>
1192
+ </SettingsSection>
988
1193
 
989
- <Button
990
- className="w-full justify-start gap-2"
991
- onClick={handleResetToDefault}
992
- variant="destructive"
1194
+ <SettingsSection
1195
+ description="Inspect how nodes are nested in this scene."
1196
+ title="Scene graph"
993
1197
  >
994
- <Trash2 className="size-4" />
995
- Clear & Start New
996
- </Button>
1198
+ <Dialog>
1199
+ <DialogTrigger asChild>
1200
+ <div className="grid gap-2 @sm:grid-cols-2">
1201
+ <Button className="w-full justify-start gap-2" variant="outline">
1202
+ <ListTree className="size-4" />
1203
+ Explore scene graph
1204
+ </Button>
1205
+ </div>
1206
+ </DialogTrigger>
1207
+ <DialogContent className="h-[80vh] max-w-[95vw] gap-0 overflow-hidden border-0 bg-[#1e1e1e] p-0 shadow-none sm:max-w-5xl">
1208
+ <DialogTitle className="sr-only">Scene Graph</DialogTitle>
1209
+ <div
1210
+ className="flex h-full min-h-0 w-full min-w-0 *:h-full *:w-full *:overflow-y-auto"
1211
+ onContextMenuCapture={blockSceneGraphMutations}
1212
+ onDragStartCapture={blockSceneGraphMutations}
1213
+ onDropCapture={blockSceneGraphMutations}
1214
+ onKeyDownCapture={blockSceneGraphDeletion}
1215
+ >
1216
+ <VisualJson value={sceneGraphValue}>
1217
+ <TreeView showCounts />
1218
+ </VisualJson>
1219
+ </div>
1220
+ </DialogContent>
1221
+ </Dialog>
1222
+ </SettingsSection>
1223
+
1224
+ <SettingsSection
1225
+ description="Removes every node from this scene. This cannot be undone."
1226
+ title="Danger zone"
1227
+ tone="destructive"
1228
+ >
1229
+ <div className="grid gap-2 @sm:grid-cols-2">
1230
+ <Button
1231
+ className="w-full justify-start gap-2"
1232
+ onClick={handleResetToDefault}
1233
+ variant="destructive"
1234
+ >
1235
+ <Trash2 className="size-4" />
1236
+ Clear and start new
1237
+ </Button>
1238
+ </div>
1239
+ </SettingsSection>
997
1240
  </div>
998
1241
  </div>
999
1242
  )