@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
@@ -19,10 +19,12 @@ import {
19
19
  SceneEnvironment,
20
20
  useViewer,
21
21
  Viewer,
22
+ type ViewerBreadcrumbHandler,
22
23
  type ViewerImmersiveSession,
23
24
  ViewerPresentations,
24
25
  } from '@pascal-app/viewer'
25
26
  import {
27
+ type CSSProperties,
26
28
  memo,
27
29
  Profiler,
28
30
  type ProfilerOnRenderCallback,
@@ -37,12 +39,21 @@ import { ViewerOverlay } from '../../components/viewer-overlay'
37
39
  import { ViewerZoneSystem } from '../../components/viewer-zone-system'
38
40
  import { type SaveStatus, useAutoSave } from '../../hooks/use-auto-save'
39
41
  import { useKeyboard } from '../../hooks/use-keyboard'
42
+ import { useIsMobile } from '../../hooks/use-mobile'
40
43
  import { useSaveShortcut } from '../../hooks/use-save-shortcut'
44
+ import { useCeilingEditSessionOwner } from '../../lib/ceiling-edit-session'
45
+ import { showsWholeBuilding, useEditorLevelDisplay } from '../../lib/editor-level-display'
46
+ import { isSceneView } from '../../lib/editor-views'
47
+ import { initFinishScriptRefresh } from '../../lib/geometry-script/finish-scripts'
48
+ import { useGestureLifecycleOwner } from '../../lib/gesture-lifecycle'
49
+ import { shouldShowEditingControls } from '../../lib/interaction/overlay-policy'
41
50
  import {
42
51
  createLocalProjectPresentationPersistence,
43
52
  type LocalProjectPresentationPersistence,
44
53
  } from '../../lib/local-project-presentation-persistence'
45
54
  import { type ActivePaintMaterial, hasActivePaintMaterial } from '../../lib/material-paint'
55
+ import { usePaintRegionHovering } from '../../lib/paint-region-hover'
56
+ import { resetPaintMode, usePaintRegionMode } from '../../lib/paint-region-mode'
46
57
  import {
47
58
  applySceneGraphToEditor,
48
59
  loadSceneFromLocalStorage,
@@ -51,20 +62,24 @@ import {
51
62
  } from '../../lib/scene'
52
63
  import { disposeSFXBus, initSFXBus } from '../../lib/sfx-bus'
53
64
  import { useUnitFocusRules } from '../../lib/units'
65
+ import { cn } from '../../lib/utils'
66
+ import { useViewerFloorDisplay } from '../../lib/viewer-selection'
54
67
  import { type CameraHintAction, useCameraHintFocus } from '../../store/use-camera-hint-focus'
55
68
  import useEditor from '../../store/use-editor'
56
69
  import useFloorplanMode from '../../store/use-floorplan-mode'
57
70
  import useSessionGroups from '../../store/use-session-groups'
71
+ import { paneOfView, VIEW_2D, VIEW_3D, type ViewPaneIndex } from '../../store/view-layout'
72
+ import { PointAskLayer } from '../point-ask/point-ask-layer'
58
73
  import { CeilingSelectionAffordanceSystem } from '../systems/ceiling/ceiling-selection-affordance-system'
59
74
  import { CeilingSystem } from '../systems/ceiling/ceiling-system'
60
75
  import { RoofEditSystem } from '../systems/roof/roof-edit-system'
61
76
  import { SelectionAffordanceManager } from '../systems/selection-affordance-manager'
62
77
  import { StairEditSystem } from '../systems/stair/stair-edit-system'
63
- import { ZoneLabelEditorSystem } from '../systems/zone/zone-label-editor-system'
64
78
  import { ZoneSystem } from '../systems/zone/zone-system'
65
79
  import { BoxSelectTool } from '../tools/select/box-select-tool'
66
80
  import { ToolManager } from '../tools/tool-manager'
67
81
  import { ActionMenu } from '../ui/action-menu'
82
+ import { type ActionMenuPlacement, TOP_MENU_CLEARANCE } from '../ui/action-menu/placement'
68
83
  import { CommandPalette, type CommandPaletteEmptyAction } from '../ui/command-palette'
69
84
  import { EditorCommands } from '../ui/command-palette/editor-commands'
70
85
  import { FloatingLevelSelector } from '../ui/floating-level-selector'
@@ -73,6 +88,7 @@ import { PanelManager } from '../ui/panels/panel-manager'
73
88
  import { ErrorBoundary } from '../ui/primitives/error-boundary'
74
89
  import { useSidebarStore } from '../ui/primitives/sidebar'
75
90
  import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/primitives/tooltip'
91
+ import { RightStack } from '../ui/right-stack'
76
92
  import { SceneLoader, SceneLoadFailed } from '../ui/scene-loader'
77
93
  import { AppSidebar } from '../ui/sidebar/app-sidebar'
78
94
  import type { ExtraPanel } from '../ui/sidebar/icon-rail'
@@ -80,6 +96,9 @@ import { SettingsPanel, type SettingsPanelProps } from '../ui/sidebar/panels/set
80
96
  import { SitePanel, type SitePanelProps } from '../ui/sidebar/panels/site-panel'
81
97
  import type { SidebarTab } from '../ui/sidebar/tab-bar'
82
98
  import { useHostPanels } from '../ui/sidebar/use-plugin-panels'
99
+ import { ViewerHoverLabel } from '../viewer/viewer-hover-label'
100
+ import { ParametricViewerRooms } from '../viewer/viewer-rooms'
101
+ import { ViewerSelectionManager } from '../viewer/viewer-selection-manager'
83
102
  import { ViewerStage } from '../viewer/viewer-stage'
84
103
  import type { ViewerStageMode } from '../viewer/viewer-stage-modes'
85
104
  import { CaptureCameraRig } from './capture-camera-rig'
@@ -93,21 +112,30 @@ import { FloatingActionMenu } from './floating-action-menu'
93
112
  import { FloatingBuildingActionMenu } from './floating-building-action-menu'
94
113
  import { FloorplanModeCoordinator } from './floorplan-mode-coordinator'
95
114
  import { FloorplanPanel } from './floorplan-panel'
115
+ import { FollowPascal } from './follow-pascal'
116
+ import { FootprintHeightHandle } from './footprint-height-handle'
96
117
  import { Grid } from './grid'
97
118
  import { GroupFloatingActionMenu } from './group-floating-action-menu'
98
119
  import { GroupRotateHandle } from './group-rotate-handle'
99
120
  import { GroupSelectionBox3D } from './group-selection-box-3d'
121
+ import { EditorHandleHitPriority } from './handles/handle-hit-priority'
100
122
  import { NodeArrowHandles } from './node-arrow-handles'
101
123
  import { QuickMeasurementHud } from './quick-measurement-hud'
102
124
  import { RiserDiagramPanel } from './riser-diagram-panel'
125
+ import { RoomLabels3D } from './room-labels'
103
126
  import { SelectionManager } from './selection-manager'
104
127
  import { SiteEdgeLabels } from './site-edge-labels'
105
128
  import { SlabHoleHighlights } from './slab-hole-highlights'
106
129
  import { SnapshotCaptureOverlay } from './snapshot-capture-overlay'
107
130
  import { type SnapshotCameraData, ThumbnailGenerator } from './thumbnail-generator'
131
+ import { VectorEdgeExtractor } from './vector-edge-extractor'
132
+ import { HostView } from './views/host-view'
133
+ import { SceneRegion } from './views/scene-region'
134
+ import { useActiveViewLayout, useEditorViews } from './views/use-editor-views'
108
135
  import { WallMeasurementLabel } from './wall-measurement-label'
109
136
  import { WallMoveSideHandles } from './wall-move-side-handles'
110
137
  import { WallOpeningHighlights } from './wall-opening-highlights'
138
+ import { WallRegionHandles } from './wall-region-handles'
111
139
 
112
140
  const CAMERA_CONTROLS_HINT_DISMISSED_STORAGE_KEY = 'editor-camera-controls-hint-dismissed:v1'
113
141
  const PREVIEW_STAGE_SWITCHER_POSITION =
@@ -117,12 +145,16 @@ const DELETE_CURSOR_BADGE_OFFSET_X = 14
117
145
  const DELETE_CURSOR_BADGE_OFFSET_Y = 14
118
146
  const PAINT_CURSOR_BADGE_COLOR = '#818cf8'
119
147
  const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
148
+ // Neutral: erasing paints no colour.
149
+ const ERASE_CURSOR_BADGE_COLOR = '#e4e4e7'
120
150
  const PAINT_CURSOR_BADGE_OFFSET_X = 14
121
151
  const PAINT_CURSOR_BADGE_OFFSET_Y = 14
122
152
  const SCENE_READY_FALLBACK_MS = 8000
123
153
  const PRESENTATION_PROJECT_NOT_RESTORED = Symbol('presentation-project-not-restored')
124
154
  const useClientLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect
125
- type PaintCursorBadgeState = 'empty' | 'ready' | 'blocked'
155
+ // `aim`: a region sub-mode off what it draws on — the HUD says where to go; no
156
+ // forbidden sign, since nothing is refused.
157
+ type PaintCursorBadgeState = 'empty' | 'ready' | 'aim' | 'blocked'
126
158
  const recordEditorRender: ProfilerOnRenderCallback = (_id, _phase, actualDuration) => {
127
159
  if (PERF_OVERLAY_ENABLED) recordPerfSample('react-render', actualDuration)
128
160
  }
@@ -130,6 +162,10 @@ const EDITOR_HOVER_STYLES: HoverStyles = {
130
162
  default: { visibleColor: 0x00_aa_ff, hiddenColor: 0xf3_ff_47, strength: 5, pulse: true },
131
163
  delete: { visibleColor: 0xef_44_44, hiddenColor: 0x99_1b_1b, strength: 6, pulse: false },
132
164
  'paint-ready': { visibleColor: 0xf5_9e_0b, hiddenColor: 0xfd_e0_68, strength: 5, pulse: true },
165
+ // Erasing paints nothing: a neutral outline, no paint colour.
166
+ 'erase-ready': { visibleColor: 0xe4_e4_e7, hiddenColor: 0xa1_a1_aa, strength: 5, pulse: false },
167
+ // Point and ask: the agent accent, steady (nothing animates a hover), the occluded parts faint.
168
+ point: { visibleColor: 0xa9_9b_ff, hiddenColor: 0x5b_4f_c9, strength: 5, pulse: false },
133
169
  'paint-disabled': {
134
170
  visibleColor: 0x94_a3_b8,
135
171
  hiddenColor: 0x47_55_69,
@@ -149,11 +185,13 @@ const EDITOR_DEFAULT_RENDER = { shading: 'solid' } as const
149
185
  function initializeEditorRuntime(): () => void {
150
186
  const unsubscribeSpatialGrid = initSpatialGridSync()
151
187
  const unsubscribeSpaceDetection = initSpaceDetectionSync(useScene, useEditor)
188
+ const stopFinishScriptRefresh = initFinishScriptRefresh()
152
189
  initSFXBus()
153
190
 
154
191
  return () => {
155
192
  unsubscribeSpatialGrid()
156
193
  unsubscribeSpaceDetection?.()
194
+ stopFinishScriptRefresh()
157
195
 
158
196
  spatialGridManager.clear()
159
197
  disposeSFXBus()
@@ -166,6 +204,8 @@ function initializeEditorRuntime(): () => void {
166
204
  export interface EditorProps {
167
205
  // Layout version — 'v1' (default) or 'v2' (navbar + two-column)
168
206
  layoutVersion?: 'v1' | 'v2'
207
+ // Viewport edge the tool menu docks to (desktop only; mobile stays at the bottom)
208
+ actionMenuPlacement?: ActionMenuPlacement
169
209
 
170
210
  // UI slots (v1)
171
211
  appMenuButton?: ReactNode
@@ -177,11 +217,20 @@ export interface EditorProps {
177
217
  viewerToolbarLeft?: ReactNode
178
218
  viewerToolbarRight?: ReactNode
179
219
  /**
180
- * Full-bleed surface swapped in over the 3D canvas (v2) — e.g. the studio
181
- * gallery. The canvas stays mounted underneath (no WebGL re-init) and the
182
- * viewer toolbar stays on top so the host's stage switch remains reachable.
220
+ * Full-bleed surface swapped in over the whole stage (v2) — e.g. a builder
221
+ * with its own canvas. The scene stays mounted underneath (no WebGL re-init)
222
+ * and the view bar steps aside. Something to show *beside* the scene is a
223
+ * view instead (`registerEditorHostView`).
183
224
  */
184
225
  stageOverlay?: ReactNode
226
+ /**
227
+ * Chrome that belongs to the scene (v2), positioned over the 3D and 2D panes
228
+ * with the level selector and tool dock — e.g. a capture bar. Hidden while
229
+ * no scene view is on screen.
230
+ */
231
+ sceneOverlay?: ReactNode
232
+ /** Show each pane's view bar (v2, default true). Off for a host that presents one fixed view. */
233
+ viewBar?: boolean
185
234
  /**
186
235
  * Docked below the node inspector (v2). Hosts mount the "save as preset"
187
236
  * affordance here so it reads as part of the inspector surface and shows
@@ -222,13 +271,23 @@ export interface EditorProps {
222
271
  // Loading indicator (e.g. project fetching in community mode)
223
272
  isLoading?: boolean
224
273
 
225
- // Fires when the full-screen scene loader shows/hides — lets hosts measure
274
+ /**
275
+ * Replaces the loading surface inside the canvas panes in both layouts.
276
+ * Loading failures always use the editor's error/retry surface.
277
+ * Omit (or pass null) to use the default SceneLoader.
278
+ */
279
+ loadingSlot?: ReactNode
280
+
281
+ // Fires when scene loading starts/finishes — lets hosts measure
226
282
  // open-to-interactive time without reaching into internal loader state.
227
283
  onLoaderChange?: (visible: boolean) => void
228
284
 
229
285
  // Thumbnail
230
286
  onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
231
287
 
288
+ /** Viewer diagnostics for the host's error-reporter trail; see `<Viewer onBreadcrumb>`. */
289
+ onViewerBreadcrumb?: ViewerBreadcrumbHandler
290
+
232
291
  /**
233
292
  * When true, skip the viewer post-FX pipeline (same as `?disable=postFx`).
234
293
  * Hosts use this for a stable local "light preview" without relying only on
@@ -243,6 +302,9 @@ export interface EditorProps {
243
302
  sidebarOverlay?: ReactNode
244
303
  viewerBanner?: ReactNode
245
304
 
305
+ /** Shown in the preview header. */
306
+ projectName?: string | null
307
+
246
308
  // Panel config (passed through to sidebar panels — v1 only)
247
309
  settingsPanelProps?: SettingsPanelProps
248
310
  sitePanelProps?: SitePanelProps
@@ -422,7 +484,7 @@ type CameraControlHint = {
422
484
  alternativeKeys?: ShortcutKey[]
423
485
  }
424
486
 
425
- const EDITOR_CAMERA_CONTROL_HINTS: CameraControlHint[] = [
487
+ const CAMERA_CONTROL_HINTS: CameraControlHint[] = [
426
488
  {
427
489
  action: 'Pan',
428
490
  keys: [{ value: 'Space' }, { value: 'Left click' }],
@@ -432,12 +494,6 @@ const EDITOR_CAMERA_CONTROL_HINTS: CameraControlHint[] = [
432
494
  { action: 'Zoom', keys: [{ value: 'Scroll' }] },
433
495
  ]
434
496
 
435
- const PREVIEW_CAMERA_CONTROL_HINTS: CameraControlHint[] = [
436
- { action: 'Pan', keys: [{ value: 'Left click' }] },
437
- { action: 'Rotate', keys: [{ value: 'Right click' }] },
438
- { action: 'Zoom', keys: [{ value: 'Scroll' }] },
439
- ]
440
-
441
497
  const CAMERA_SHORTCUT_KEY_META: Record<string, { icon?: string; label: string; text?: string }> = {
442
498
  'Left click': {
443
499
  icon: 'ph:mouse-left-click-fill',
@@ -545,13 +601,13 @@ function CameraControlHintItem({ hint }: { hint: CameraControlHint }) {
545
601
  }
546
602
 
547
603
  function ViewerCanvasControlsHint({
548
- isPreviewMode,
604
+ belowTopMenu = false,
549
605
  onDismiss,
550
606
  }: {
551
- isPreviewMode: boolean
607
+ belowTopMenu?: boolean
552
608
  onDismiss: () => void
553
609
  }) {
554
- const all = isPreviewMode ? PREVIEW_CAMERA_CONTROL_HINTS : EDITOR_CAMERA_CONTROL_HINTS
610
+ const all = CAMERA_CONTROL_HINTS
555
611
  // A host teaching one gesture at a time narrows this to the one it is asking
556
612
  // for, and to nothing once it is done. Null — the default — is all of them.
557
613
  const focus = useCameraHintFocus((state) => state.actions)
@@ -562,7 +618,13 @@ function ViewerCanvasControlsHint({
562
618
  }
563
619
 
564
620
  return (
565
- <div className="pointer-events-none absolute top-14 left-1/2 z-40 max-w-[calc(100%-2rem)] -translate-x-1/2">
621
+ <div
622
+ className={cn(
623
+ 'pointer-events-none absolute left-1/2 z-40 max-w-[calc(100%-2rem)] -translate-x-1/2',
624
+ !belowTopMenu && 'top-14',
625
+ )}
626
+ style={belowTopMenu ? { top: TOP_MENU_CLEARANCE } : undefined}
627
+ >
566
628
  <section
567
629
  aria-label="Camera controls hint"
568
630
  className="pointer-events-auto flex items-start gap-3 rounded-2xl border border-border/35 bg-background/90 px-3.5 py-2.5 shadow-elevation-4 backdrop-blur-xl"
@@ -680,26 +742,31 @@ function PaintCursorBadge({
680
742
  swatchColor,
681
743
  swatchImageUrl,
682
744
  isEraser,
745
+ isPicker = false,
683
746
  }: {
684
747
  position: { x: number; y: number }
685
748
  state: PaintCursorBadgeState
686
749
  swatchColor: string
687
750
  swatchImageUrl?: string
688
751
  isEraser: boolean
752
+ /** The eyedropper: the swatch is what a click would take. */
753
+ isPicker?: boolean
689
754
  }) {
690
755
  const accentColor =
691
756
  state === 'ready'
692
757
  ? isEraser
693
- ? PAINT_CURSOR_BADGE_COLOR
758
+ ? ERASE_CURSOR_BADGE_COLOR
694
759
  : swatchColor
695
760
  : PAINT_CURSOR_BADGE_DISABLED_COLOR
696
- const iconOpacity = state === 'ready' ? 1 : state === 'blocked' ? 0.62 : 0.42
761
+ const iconOpacity = state === 'ready' ? 1 : state === 'blocked' || state === 'aim' ? 0.62 : 0.42
697
762
  const lineHeight = 18
698
763
 
699
764
  return (
700
765
  <div
701
766
  aria-hidden="true"
702
767
  className="pointer-events-none absolute z-40"
768
+ data-paint-cursor-state={state}
769
+ data-paint-cursor-tool={isEraser ? 'erase' : isPicker ? 'pick' : 'paint'}
703
770
  style={{
704
771
  left: position.x,
705
772
  top: position.y,
@@ -721,29 +788,32 @@ function PaintCursorBadge({
721
788
  transform: `translate(-50%, calc(-100% - ${lineHeight}px))`,
722
789
  }}
723
790
  >
724
- {/* eslint-disable-next-line @next/next/no-img-element */}
725
- <img
726
- alt=""
727
- aria-hidden="true"
728
- className="h-5 w-5 object-contain drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
729
- src="/icons/paint.webp"
730
- style={{
731
- filter: state === 'ready' ? undefined : 'grayscale(1)',
732
- opacity: iconOpacity,
733
- }}
734
- />
735
- {state === 'ready' ? (
736
- isEraser ? (
737
- <span className="-right-1 -bottom-1 absolute flex h-3.5 w-3.5 items-center justify-center rounded-full border border-white/35 bg-zinc-950 text-white shadow-[0_2px_6px_rgba(0,0,0,0.45)]">
738
- <Icon
739
- aria-hidden="true"
740
- color="currentColor"
741
- height={10}
742
- icon="mdi:eraser-variant"
743
- width={10}
744
- />
745
- </span>
746
- ) : (
791
+ {isPicker || isEraser ? (
792
+ // The eyedropper and the eraser carry their own tool, never a paint colour.
793
+ <Icon
794
+ aria-hidden="true"
795
+ className="text-white"
796
+ color="currentColor"
797
+ height={18}
798
+ icon={isEraser ? 'mdi:eraser-variant' : 'lucide:pipette'}
799
+ style={{ opacity: iconOpacity }}
800
+ width={18}
801
+ />
802
+ ) : (
803
+ /* eslint-disable-next-line @next/next/no-img-element */
804
+ <img
805
+ alt=""
806
+ aria-hidden="true"
807
+ className="h-5 w-5 object-contain drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
808
+ src="/icons/paint.webp"
809
+ style={{
810
+ filter: state === 'ready' ? undefined : 'grayscale(1)',
811
+ opacity: iconOpacity,
812
+ }}
813
+ />
814
+ )}
815
+ {state === 'ready' || state === 'aim' ? (
816
+ isEraser ? null : (
747
817
  <span
748
818
  className="-right-1 -bottom-1 absolute h-3.5 w-3.5 rounded-full border border-white/70 bg-cover bg-center shadow-[0_2px_6px_rgba(0,0,0,0.45)]"
749
819
  style={{
@@ -751,6 +821,7 @@ function PaintCursorBadge({
751
821
  backgroundImage: swatchImageUrl
752
822
  ? `url(${JSON.stringify(swatchImageUrl)})`
753
823
  : undefined,
824
+ opacity: state === 'aim' ? 0.55 : 1,
754
825
  }}
755
826
  />
756
827
  )
@@ -815,16 +886,20 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
815
886
  <SceneEnvironment />
816
887
  {!(isFirstPersonMode || isStudioMode || isCaptureMode) && <SelectionManager />}
817
888
  {!(noEditing || isXRMode) && <BoxSelectTool />}
889
+ {!noEditing && <EditorHandleHitPriority />}
818
890
  {!noEditing && <NodeArrowHandles />}
891
+ {!noEditing && <FootprintHeightHandle />}
819
892
  {!noEditing && <GroupRotateHandle />}
820
893
  {!noEditing && <GroupSelectionBox3D />}
821
894
  {!noEditing && <WallOpeningHighlights />}
822
895
  {!noEditing && <SlabHoleHighlights />}
823
896
  {!noEditing && <WallMoveSideHandles />}
897
+ {!noEditing && <WallRegionHandles />}
824
898
  {!noEditing && <FenceTangentLines3D />}
825
899
  {!(noEditing || isXRMode) && <FloatingActionMenu />}
826
900
  {!(noEditing || isXRMode) && <GroupFloatingActionMenu />}
827
901
  {!(noEditing || isXRMode) && <FloatingBuildingActionMenu />}
902
+ {!(noEditing || isXRMode) && <RoomLabels3D />}
828
903
  {!(isFirstPersonMode || isXRMode) && <WallMeasurementLabel />}
829
904
  <ExportManager />
830
905
  {isFirstPersonMode ? <ViewerZoneSystem /> : <ZoneSystem />}
@@ -838,9 +913,10 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
838
913
  {isFirstPersonMode && <FirstPersonControls />}
839
914
  {isCaptureMode && !isXRMode && <CaptureCameraRig />}
840
915
  {!isXRMode && <CustomCameraControls />}
916
+ {!isXRMode && <FollowPascal />}
841
917
  {!isXRMode && <ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />}
918
+ {!isXRMode && <VectorEdgeExtractor />}
842
919
  {!(isFirstPersonMode || isXRMode) && <SiteEdgeLabels />}
843
- <InteractiveSystem />
844
920
  {presentationsReady ? <ViewerPresentations /> : null}
845
921
  {!noEditing && viewerSceneSlot}
846
922
  </>
@@ -933,7 +1009,10 @@ function PaintCursorLayer({
933
1009
  }) {
934
1010
  const mode = useEditor((s) => s.mode)
935
1011
  const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
936
- const paintEraser = useEditor((s) => s.paintEraser)
1012
+ const paintMode = usePaintRegionMode((s) => s.mode)
1013
+ const picked = usePaintRegionMode((s) => s.picked)
1014
+ const paintEraser = paintMode === 'erase'
1015
+ const picking = paintMode === 'pick'
937
1016
  const paintHover = useEditor((s) => s.paintHover)
938
1017
  const sceneMaterials = useScene((s) => s.materials)
939
1018
  const [position, setPosition] = useState<{ x: number; y: number } | null>(null)
@@ -985,14 +1064,26 @@ function PaintCursorLayer({
985
1064
  }
986
1065
  }, [active, containerRef])
987
1066
 
988
- const hasPaint = paintEraser || hasActivePaintMaterial(activePaintMaterial)
1067
+ const hasPaint = paintEraser || picking || hasActivePaintMaterial(activePaintMaterial)
1068
+ // A region sub-mode reads its own surfaces (walls, floors), not the whole-surface hover.
1069
+ const regionMode = paintMode === 'rectangle' || paintMode === 'polygon'
1070
+ const regionHovering = usePaintRegionHovering()
989
1071
  const badgeState: PaintCursorBadgeState = !hasPaint
990
1072
  ? 'empty'
991
- : paintHover != null
992
- ? 'ready'
993
- : 'blocked'
994
- const swatchColor = getActivePaintMaterialSwatchColor(activePaintMaterial, sceneMaterials)
995
- const swatchImageUrl = getActivePaintMaterialSwatchImageUrl(activePaintMaterial, sceneMaterials)
1073
+ : picking
1074
+ ? picked
1075
+ ? 'ready'
1076
+ : 'aim'
1077
+ : regionMode
1078
+ ? regionHovering
1079
+ ? 'ready'
1080
+ : 'aim'
1081
+ : paintHover != null
1082
+ ? 'ready'
1083
+ : 'blocked'
1084
+ const swatchMaterial = picking ? picked : activePaintMaterial
1085
+ const swatchColor = getActivePaintMaterialSwatchColor(swatchMaterial, sceneMaterials)
1086
+ const swatchImageUrl = getActivePaintMaterialSwatchImageUrl(swatchMaterial, sceneMaterials)
996
1087
 
997
1088
  if (!active || !position) return null
998
1089
 
@@ -1003,6 +1094,7 @@ function PaintCursorLayer({
1003
1094
  >
1004
1095
  <PaintCursorBadge
1005
1096
  isEraser={paintEraser}
1097
+ isPicker={picking}
1006
1098
  position={{ x: 0, y: 0 }}
1007
1099
  state={badgeState}
1008
1100
  swatchColor={swatchColor}
@@ -1012,8 +1104,8 @@ function PaintCursorLayer({
1012
1104
  )
1013
1105
  }
1014
1106
 
1015
- // ── Viewer canvas: memoized, subscribes to viewMode/floorplanPaneRatio internally ──
1016
- // This prevents Editor from re-rendering when those values change.
1107
+ // ── Viewer canvas: memoized, subscribes to the view layout internally ──
1108
+ // This prevents Editor from re-rendering when the layout changes.
1017
1109
 
1018
1110
  const ViewerCanvas = memo(function ViewerCanvas({
1019
1111
  isVersionPreviewMode,
@@ -1022,14 +1114,18 @@ const ViewerCanvas = memo(function ViewerCanvas({
1022
1114
  isStudioMode,
1023
1115
  hasLoadedInitialScene,
1024
1116
  showLoader,
1117
+ isSceneDataLoading,
1118
+ loadingOverlay,
1025
1119
  sceneReadyKey,
1026
1120
  onSceneReadyChange,
1027
1121
  onThumbnailCapture,
1122
+ onViewerBreadcrumb,
1028
1123
  presentationsReady,
1029
1124
  viewerSceneSlot,
1030
1125
  floorplanSceneSlot,
1031
1126
  disablePostFx = false,
1032
1127
  immersive,
1128
+ hintBelowTopMenu = false,
1033
1129
  }: {
1034
1130
  isVersionPreviewMode: boolean
1035
1131
  isLoading: boolean
@@ -1037,21 +1133,33 @@ const ViewerCanvas = memo(function ViewerCanvas({
1037
1133
  isStudioMode: boolean
1038
1134
  hasLoadedInitialScene: boolean
1039
1135
  showLoader: boolean
1136
+ isSceneDataLoading: boolean
1137
+ loadingOverlay: ReactNode
1040
1138
  sceneReadyKey: number
1041
1139
  onSceneReadyChange: (ready: boolean) => void
1042
1140
  onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
1141
+ onViewerBreadcrumb?: ViewerBreadcrumbHandler
1043
1142
  presentationsReady: boolean
1044
1143
  viewerSceneSlot?: ReactNode
1045
1144
  floorplanSceneSlot?: ReactNode
1046
1145
  disablePostFx?: boolean
1047
1146
  immersive?: ViewerImmersiveSession
1147
+ hintBelowTopMenu?: boolean
1048
1148
  }) {
1049
- const viewMode = useEditor((s) => s.viewMode)
1050
- const floorplanPaneRatio = useEditor((s) => s.floorplanPaneRatio)
1051
- const setFloorplanPaneRatio = useEditor((s) => s.setFloorplanPaneRatio)
1149
+ const layout = useActiveViewLayout()
1150
+ const views = useEditorViews()
1052
1151
  const isPreviewMode = useEditor((s) => s.isPreviewMode)
1053
1152
  const isCaptureMode = useEditor((s) => s.isCaptureMode)
1054
1153
  useUnitFocusRules()
1154
+ const wholeBuilding = useEditor((s) =>
1155
+ showsWholeBuilding({
1156
+ isPreviewMode: s.isPreviewMode,
1157
+ isFirstPersonMode,
1158
+ captureMode: s.captureMode,
1159
+ captureLevelId: s.captureLevelId,
1160
+ }),
1161
+ )
1162
+ useEditorLevelDisplay(wholeBuilding)
1055
1163
  const presetIsolation = useEditor((s) =>
1056
1164
  s.captureMode.mode === 'preset' ? s.captureMode.isolated : null,
1057
1165
  )
@@ -1069,25 +1177,24 @@ const ViewerCanvas = memo(function ViewerCanvas({
1069
1177
  setViewerAreaEl(el)
1070
1178
  }, [])
1071
1179
  const viewer3dRef = useRef<HTMLDivElement>(null)
1072
- const isResizingFloorplan = useRef(false)
1180
+ const isResizingPanes = useRef(false)
1073
1181
 
1074
- const handleFloorplanDividerDown = useCallback((e: React.PointerEvent) => {
1182
+ const handlePaneDividerDown = useCallback((e: React.PointerEvent) => {
1075
1183
  e.preventDefault()
1076
- isResizingFloorplan.current = true
1184
+ isResizingPanes.current = true
1077
1185
  document.body.style.cursor = 'col-resize'
1078
1186
  document.body.style.userSelect = 'none'
1079
1187
  }, [])
1080
1188
 
1081
1189
  useEffect(() => {
1082
1190
  const handlePointerMove = (e: PointerEvent) => {
1083
- if (!isResizingFloorplan.current) return
1191
+ if (!isResizingPanes.current) return
1084
1192
  if (!viewerAreaRef.current) return
1085
1193
  const rect = viewerAreaRef.current.getBoundingClientRect()
1086
- const newRatio = (e.clientX - rect.left) / rect.width
1087
- setFloorplanPaneRatio(Math.max(0.15, Math.min(0.85, newRatio)))
1194
+ useEditor.getState().setViewPaneRatio((e.clientX - rect.left) / rect.width)
1088
1195
  }
1089
1196
  const handlePointerUp = () => {
1090
- isResizingFloorplan.current = false
1197
+ isResizingPanes.current = false
1091
1198
  document.body.style.cursor = ''
1092
1199
  document.body.style.userSelect = ''
1093
1200
  }
@@ -1097,7 +1204,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
1097
1204
  window.removeEventListener('pointermove', handlePointerMove)
1098
1205
  window.removeEventListener('pointerup', handlePointerUp)
1099
1206
  }
1100
- }, [setFloorplanPaneRatio])
1207
+ }, [])
1101
1208
 
1102
1209
  useEffect(() => {
1103
1210
  setIsCameraControlsHintVisible(!readCameraControlsHintDismissed())
@@ -1108,91 +1215,141 @@ const ViewerCanvas = memo(function ViewerCanvas({
1108
1215
  writeCameraControlsHintDismissed(true)
1109
1216
  }, [])
1110
1217
 
1111
- const show2d = viewMode === '2d' || viewMode === 'split'
1112
- const show3d = viewMode === '3d' || viewMode === 'split'
1218
+ const pane2d = paneOfView(layout, VIEW_2D)
1219
+ const pane3d = paneOfView(layout, VIEW_3D)
1220
+ const show3d = pane3d !== null
1221
+ // Panes are grid cells placed by `gridColumn`, in a fixed DOM order: moving a
1222
+ // view to the other pane restyles it rather than reparenting it, so the 3D
1223
+ // canvas keeps its WebGL context and the plan its session through a swap.
1224
+ const paneStyle = (pane: ViewPaneIndex | null): CSSProperties =>
1225
+ pane === null ? { display: 'none' } : { gridColumn: pane + 1, gridRow: 1 }
1226
+ const hostPanes = ([0, 1] as const).filter(
1227
+ (pane) => (pane === 0 || layout.split) && !isSceneView(layout.panes[pane]),
1228
+ )
1229
+ const focusPane = (pane: ViewPaneIndex) => () => {
1230
+ if (layout.split) useEditor.getState().focusViewPane(pane)
1231
+ }
1113
1232
 
1114
1233
  return (
1115
1234
  <ErrorBoundary fallback={<EditorSceneCrashFallback />}>
1116
1235
  {/* `relative` so the floorplan compass (portaled here to stay visible in
1117
- 2d / 3d / split alike) can anchor to this container's bottom-left. */}
1118
- <div className="relative flex h-full" ref={setViewerAreaNode}>
1236
+ every layout) can anchor to this container's bottom-left. */}
1237
+ <div className="relative h-full" ref={setViewerAreaNode}>
1119
1238
  <QuickMeasurementHud />
1120
1239
  <DeleteConfirmationDialog />
1121
- {/* 2D floorplan — always mounted once shown, hidden via CSS to preserve state */}
1122
1240
  <div
1123
- className="relative h-full flex-shrink-0"
1241
+ className="absolute inset-0 grid"
1242
+ data-view-split={layout.split || undefined}
1124
1243
  style={{
1125
- width: viewMode === '2d' ? '100%' : `${floorplanPaneRatio * 100}%`,
1126
- display: show2d ? undefined : 'none',
1244
+ gridTemplateColumns: layout.split
1245
+ ? `minmax(0, ${layout.ratio}fr) minmax(0, ${1 - layout.ratio}fr)`
1246
+ : 'minmax(0, 1fr)',
1247
+ gridTemplateRows: 'minmax(0, 1fr)',
1127
1248
  }}
1128
1249
  >
1129
- <div className="h-full w-full overflow-hidden">
1130
- <FloorplanPanel compassHost={viewerAreaEl} floorplanSceneSlot={floorplanSceneSlot} />
1250
+ {/* 2D floorplan — always mounted once shown, hidden via CSS to preserve state */}
1251
+ <div
1252
+ aria-busy={isSceneDataLoading}
1253
+ className="relative min-w-0 overflow-hidden"
1254
+ data-pascal-viewer-2d
1255
+ data-view-pane={pane2d ?? undefined}
1256
+ onPointerDownCapture={pane2d === null ? undefined : focusPane(pane2d)}
1257
+ style={paneStyle(pane2d)}
1258
+ >
1259
+ <div className="relative isolate h-full w-full overflow-hidden">
1260
+ <FloorplanPanel compassHost={viewerAreaEl} floorplanSceneSlot={floorplanSceneSlot} />
1261
+ </div>
1262
+ {isSceneDataLoading && loadingOverlay}
1131
1263
  </div>
1132
- {viewMode === 'split' && (
1133
- <div
1134
- className="absolute inset-y-0 -right-3 z-10 flex w-6 cursor-col-resize items-center justify-center"
1135
- onPointerDown={handleFloorplanDividerDown}
1136
- >
1137
- <div className="h-8 w-1 rounded-full bg-neutral-400" />
1264
+
1265
+ {/* 3D viewer — always mounted, hidden via CSS to avoid destroying the WebGL context */}
1266
+ <div
1267
+ aria-busy={showLoader}
1268
+ className="relative min-w-0 overflow-hidden"
1269
+ data-pascal-viewer-3d
1270
+ data-view-pane={pane3d ?? undefined}
1271
+ onPointerDownCapture={pane3d === null ? undefined : focusPane(pane3d)}
1272
+ ref={viewer3dRef}
1273
+ style={paneStyle(pane3d)}
1274
+ >
1275
+ <div className="relative isolate h-full">
1276
+ <DeleteCursorLayer
1277
+ containerRef={viewer3dRef}
1278
+ isVersionPreviewMode={isVersionPreviewMode}
1279
+ />
1280
+ <PaintCursorLayer
1281
+ containerRef={viewer3dRef}
1282
+ isVersionPreviewMode={isVersionPreviewMode}
1283
+ />
1284
+ {!isVersionPreviewMode && <PointAskLayer />}
1285
+ {!showLoader && isCameraControlsHintVisible && !isFirstPersonMode ? (
1286
+ <ViewerCanvasControlsHint
1287
+ belowTopMenu={hintBelowTopMenu}
1288
+ onDismiss={dismissCameraControlsHint}
1289
+ />
1290
+ ) : null}
1291
+ <SelectionPersistenceManager enabled={hasLoadedInitialScene && !showLoader} />
1292
+ <Viewer
1293
+ defaultRender={EDITOR_DEFAULT_RENDER}
1294
+ disablePostFx={disablePostFx}
1295
+ hoverStyles={EDITOR_HOVER_STYLES}
1296
+ isolate={presetIsolation}
1297
+ // Preset captures isolate one subtree and keep the exterior transparent.
1298
+ // Other modes retain the viewer's configured background policy.
1299
+ transparent={presetIsolation === null ? undefined : true}
1300
+ onSceneReadyChange={onSceneReadyChange}
1301
+ onBreadcrumb={onViewerBreadcrumb}
1302
+ renderContext="editor"
1303
+ renderPaused={!show3d && !showLoader}
1304
+ sceneReadyKey={sceneReadyKey}
1305
+ immersive={immersive}
1306
+ // Walk/drone framing during snapshot capture is camera-only: the
1307
+ // viewer's default selection manager would hover-highlight whatever
1308
+ // the cursor crosses, which orbit capture never does.
1309
+ selectionManager={isFirstPersonMode && !isCaptureMode ? 'default' : 'custom'}
1310
+ >
1311
+ <ViewerSceneContent
1312
+ isFirstPersonMode={isFirstPersonMode}
1313
+ isLoading={showLoader}
1314
+ isXRMode={immersive != null}
1315
+ isStudioMode={isStudioMode}
1316
+ isVersionPreviewMode={isVersionPreviewMode}
1317
+ onThumbnailCapture={onThumbnailCapture}
1318
+ presentationsReady={presentationsReady}
1319
+ viewerSceneSlot={viewerSceneSlot}
1320
+ />
1321
+ </Viewer>
1138
1322
  </div>
1139
- )}
1323
+ {showLoader && loadingOverlay}
1324
+ </div>
1325
+
1326
+ {hostPanes.map((pane) => {
1327
+ const viewId = layout.panes[pane]
1328
+ return (
1329
+ <div
1330
+ className="relative min-w-0 overflow-hidden"
1331
+ data-view-pane={pane}
1332
+ key={viewId}
1333
+ onFocusCapture={focusPane(pane)}
1334
+ onPointerDownCapture={focusPane(pane)}
1335
+ style={paneStyle(pane)}
1336
+ >
1337
+ <HostView key={viewId} view={views.find((view) => view.id === viewId)} />
1338
+ </div>
1339
+ )
1340
+ })}
1140
1341
  </div>
1141
1342
 
1142
- {/* 3D viewer — always mounted, hidden via CSS to avoid destroying the WebGL context */}
1143
- <div
1144
- className="relative min-w-0 flex-1 overflow-hidden"
1145
- data-pascal-viewer-3d
1146
- ref={viewer3dRef}
1147
- style={{ display: show3d ? undefined : 'none' }}
1148
- >
1149
- <DeleteCursorLayer
1150
- containerRef={viewer3dRef}
1151
- isVersionPreviewMode={isVersionPreviewMode}
1152
- />
1153
- <PaintCursorLayer
1154
- containerRef={viewer3dRef}
1155
- isVersionPreviewMode={isVersionPreviewMode}
1156
- />
1157
- {!showLoader && isCameraControlsHintVisible && !isFirstPersonMode ? (
1158
- <ViewerCanvasControlsHint
1159
- isPreviewMode={isPreviewMode}
1160
- onDismiss={dismissCameraControlsHint}
1161
- />
1162
- ) : null}
1163
- <SelectionPersistenceManager enabled={hasLoadedInitialScene && !showLoader} />
1164
- <Viewer
1165
- defaultRender={EDITOR_DEFAULT_RENDER}
1166
- disablePostFx={disablePostFx}
1167
- hoverStyles={EDITOR_HOVER_STYLES}
1168
- isolate={presetIsolation}
1169
- // Preset captures isolate one subtree and keep the exterior transparent.
1170
- // Other modes retain the viewer's configured background policy.
1171
- transparent={presetIsolation === null ? undefined : true}
1172
- onSceneReadyChange={onSceneReadyChange}
1173
- renderContext="editor"
1174
- renderPaused={!show3d && !showLoader}
1175
- sceneReadyKey={sceneReadyKey}
1176
- immersive={immersive}
1177
- // Walk/drone framing during snapshot capture is camera-only: the
1178
- // viewer's default selection manager would hover-highlight whatever
1179
- // the cursor crosses, which orbit capture never does.
1180
- selectionManager={isFirstPersonMode && !isCaptureMode ? 'default' : 'custom'}
1343
+ {layout.split && (
1344
+ <div
1345
+ className="absolute inset-y-0 z-10 flex w-6 -translate-x-1/2 cursor-col-resize items-center justify-center"
1346
+ onPointerDown={handlePaneDividerDown}
1347
+ style={{ left: `${layout.ratio * 100}%` }}
1181
1348
  >
1182
- <ViewerSceneContent
1183
- isFirstPersonMode={isFirstPersonMode}
1184
- isLoading={showLoader}
1185
- isXRMode={immersive != null}
1186
- isStudioMode={isStudioMode}
1187
- isVersionPreviewMode={isVersionPreviewMode}
1188
- onThumbnailCapture={onThumbnailCapture}
1189
- presentationsReady={presentationsReady}
1190
- viewerSceneSlot={viewerSceneSlot}
1191
- />
1192
- </Viewer>
1193
- </div>
1349
+ <div className="h-8 w-1 rounded-full bg-neutral-400" />
1350
+ </div>
1351
+ )}
1194
1352
  </div>
1195
- {!(showLoader || isVersionPreviewMode) && <ZoneLabelEditorSystem />}
1196
1353
  </ErrorBoundary>
1197
1354
  )
1198
1355
  })
@@ -1201,13 +1358,17 @@ function PreviewStage({
1201
1358
  isFirstPersonMode,
1202
1359
  mode,
1203
1360
  onModeChange,
1361
+ projectName,
1204
1362
  showLoader,
1363
+ loadingOverlay,
1205
1364
  viewerContent,
1206
1365
  }: {
1207
1366
  isFirstPersonMode: boolean
1208
1367
  mode: ViewerStageMode
1209
1368
  onModeChange: (mode: ViewerStageMode) => void
1369
+ projectName?: string | null
1210
1370
  showLoader: boolean
1371
+ loadingOverlay: ReactNode
1211
1372
  viewerContent: ReactNode
1212
1373
  }) {
1213
1374
  const hasFloorplan = useScene((state) =>
@@ -1222,6 +1383,7 @@ function PreviewStage({
1222
1383
  [onModeChange],
1223
1384
  )
1224
1385
 
1386
+ useViewerFloorDisplay()
1225
1387
  const stageMode = isFirstPersonMode || !hasFloorplan ? '3d' : mode
1226
1388
  const stageModes = hasFloorplan && !isFirstPersonMode ? undefined : (['3d'] as const)
1227
1389
 
@@ -1233,9 +1395,11 @@ function PreviewStage({
1233
1395
  <ViewerOverlay
1234
1396
  hideBottomBar={stageMode !== '3d'}
1235
1397
  onBack={() => useEditor.getState().setPreviewMode(false)}
1398
+ projectName={projectName}
1236
1399
  />
1237
1400
  )}
1238
1401
 
1402
+ {stageMode === '3d' && !isFirstPersonMode ? <ViewerHoverLabel /> : null}
1239
1403
  <ViewerStage
1240
1404
  className="absolute inset-0"
1241
1405
  mode={stageMode}
@@ -1246,6 +1410,7 @@ function PreviewStage({
1246
1410
  switcherClassName={`${PREVIEW_STAGE_SWITCHER_POSITION} ${showLoader ? 'z-[70]' : ''}`}
1247
1411
  >
1248
1412
  {viewerContent}
1413
+ {showLoader && loadingOverlay}
1249
1414
  </ViewerStage>
1250
1415
  </div>
1251
1416
  )
@@ -1254,6 +1419,7 @@ function PreviewStage({
1254
1419
  function EditorContent({
1255
1420
  guardAgainstSceneWipe,
1256
1421
  layoutVersion = 'v1',
1422
+ actionMenuPlacement,
1257
1423
  appMenuButton,
1258
1424
  sidebarTop,
1259
1425
  navbarSlot,
@@ -1261,6 +1427,8 @@ function EditorContent({
1261
1427
  viewerToolbarLeft,
1262
1428
  viewerToolbarRight,
1263
1429
  stageOverlay,
1430
+ sceneOverlay,
1431
+ viewBar = true,
1264
1432
  inspectorFooter,
1265
1433
  multiSelectionFooter,
1266
1434
  viewerSceneSlot,
@@ -1274,17 +1442,23 @@ function EditorContent({
1274
1442
  previewScene,
1275
1443
  isVersionPreviewMode = false,
1276
1444
  isLoading = false,
1445
+ loadingSlot,
1277
1446
  onLoaderChange,
1278
1447
  onThumbnailCapture,
1448
+ onViewerBreadcrumb,
1279
1449
  disablePostFx = false,
1280
1450
  immersive,
1281
1451
  sidebarOverlay,
1282
1452
  viewerBanner,
1453
+ projectName,
1283
1454
  settingsPanelProps,
1284
1455
  sitePanelProps,
1285
1456
  extraSidebarPanels,
1286
1457
  commandPaletteEmptyAction,
1287
1458
  }: EditorProps) {
1459
+ const isMobile = useIsMobile()
1460
+ // A top-docked menu sits at the top of the scene panes on desktop (layout v2).
1461
+ const dockMenuOnTop = actionMenuPlacement === 'top' && !isMobile
1288
1462
  const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
1289
1463
  const isStudioMode = useEditor((s) => s.workspaceMode === 'studio')
1290
1464
  const presentationProjectId = projectId ?? null
@@ -1311,8 +1485,9 @@ function EditorContent({
1311
1485
  }, [presentationProjectId])
1312
1486
 
1313
1487
  useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode || isStudioMode })
1488
+ useGestureLifecycleOwner()
1314
1489
 
1315
- const { isLoadingSceneRef, saveNow } = useAutoSave({
1490
+ const { beginSceneLoad, completeSceneLoad, saveNow } = useAutoSave({
1316
1491
  guardAgainstSceneWipe,
1317
1492
  onSave,
1318
1493
  onDirty,
@@ -1338,6 +1513,12 @@ function EditorContent({
1338
1513
  const [previewStageMode, setPreviewStageMode] = useState<ViewerStageMode>('3d')
1339
1514
  const isPreviewMode = useEditor((s) => s.isPreviewMode)
1340
1515
  const isCaptureMode = useEditor((s) => s.isCaptureMode)
1516
+ const readOnly = useScene((s) => s.readOnly)
1517
+ // Chrome only steps down for the top menu while the menu is actually there.
1518
+ const topMenuShown =
1519
+ dockMenuOnTop &&
1520
+ !(isVersionPreviewMode || isCaptureMode || isStudioMode) &&
1521
+ shouldShowEditingControls(readOnly)
1341
1522
 
1342
1523
  const sidebarWidth = useSidebarStore((s) => s.width)
1343
1524
  const isSidebarCollapsed = useSidebarStore((s) => s.isCollapsed)
@@ -1352,6 +1533,7 @@ function EditorContent({
1352
1533
  const teardown = initializeEditorRuntime()
1353
1534
  return teardown
1354
1535
  }, [])
1536
+ useCeilingEditSessionOwner()
1355
1537
 
1356
1538
  useEffect(() => {
1357
1539
  void useEditor.persist.rehydrate()
@@ -1361,6 +1543,7 @@ function EditorContent({
1361
1543
  useEffect(() => {
1362
1544
  useViewer.getState().setProjectId(projectId ?? null)
1363
1545
  useFloorplanMode.getState().setProjectId(projectId ?? null)
1546
+ resetPaintMode()
1364
1547
 
1365
1548
  return () => {
1366
1549
  useViewer.getState().setProjectId(null)
@@ -1371,9 +1554,13 @@ function EditorContent({
1371
1554
  // Load on mount, project switches, and explicit retry attempts.
1372
1555
  useEffect(() => {
1373
1556
  let cancelled = false
1557
+ // The shell stays usable while loading; scene commands must not edit the
1558
+ // unloaded graph. Release synchronously before hydration so its normal
1559
+ // migrations and derived geometry can write, and retain the lock on error.
1560
+ let releaseLoadingLock = acquireSceneReadOnlyLease()
1374
1561
 
1375
1562
  async function load(attempt: number) {
1376
- isLoadingSceneRef.current = true
1563
+ beginSceneLoad()
1377
1564
  setSceneLoadError(null)
1378
1565
  setHasLoadedInitialScene(false)
1379
1566
  setIsViewerSceneReady(false)
@@ -1387,7 +1574,11 @@ function EditorContent({
1387
1574
  try {
1388
1575
  const sceneGraph = onLoad ? await onLoad() : loadSceneFromLocalStorage()
1389
1576
  if (!cancelled && attempt === sceneLoadAttempt) {
1577
+ releaseLoadingLock()
1390
1578
  applySceneGraphToEditor(sceneGraph)
1579
+ // The store holds the loaded graph: autosave resumes now, not on a
1580
+ // frame — a tab loaded while hidden never gets one.
1581
+ completeSceneLoad()
1391
1582
  setIsViewerSceneReady(false)
1392
1583
  setSceneReadyKey((key) => key + 1)
1393
1584
  }
@@ -1396,6 +1587,8 @@ function EditorContent({
1396
1587
  // state: nothing may be written until a load actually succeeds.
1397
1588
  failed = true
1398
1589
  if (!cancelled) {
1590
+ releaseLoadingLock()
1591
+ releaseLoadingLock = acquireSceneReadOnlyLease()
1399
1592
  console.error('[editor] scene load failed', error)
1400
1593
  setSceneLoadError(error ?? new Error('Scene load failed'))
1401
1594
  }
@@ -1404,9 +1597,11 @@ function EditorContent({
1404
1597
  setIsSceneLoading(false)
1405
1598
  if (!failed) {
1406
1599
  setHasLoadedInitialScene(true)
1407
- requestAnimationFrame(() => {
1408
- isLoadingSceneRef.current = false
1409
- })
1600
+ // A project opens in select mode. Rehydrate already drops the
1601
+ // persisted tool, but a host's rail panel may arm a mode of its
1602
+ // own when it mounts, and with a restored rail tab that mount can
1603
+ // land after rehydrate.
1604
+ useEditor.getState().armToolMode({ mode: 'select' })
1410
1605
  }
1411
1606
  }
1412
1607
  }
@@ -1416,8 +1611,9 @@ function EditorContent({
1416
1611
 
1417
1612
  return () => {
1418
1613
  cancelled = true
1614
+ releaseLoadingLock()
1419
1615
  }
1420
- }, [onLoad, isLoadingSceneRef, sceneLoadAttempt])
1616
+ }, [onLoad, beginSceneLoad, completeSceneLoad, sceneLoadAttempt])
1421
1617
 
1422
1618
  const retrySceneLoad = useCallback(() => {
1423
1619
  setSceneLoadAttempt((attempt) => attempt + 1)
@@ -1445,13 +1641,6 @@ function EditorContent({
1445
1641
  if (!isPreviewMode) setPreviewStageMode('3d')
1446
1642
  }, [isPreviewMode])
1447
1643
 
1448
- useEffect(() => {
1449
- document.body.classList.add('dark')
1450
- return () => {
1451
- document.body.classList.remove('dark')
1452
- }
1453
- }, [])
1454
-
1455
1644
  const handleSceneReadyChange = useCallback((ready: boolean) => {
1456
1645
  setIsViewerSceneReady(ready)
1457
1646
  }, [])
@@ -1469,7 +1658,8 @@ function EditorContent({
1469
1658
  return () => window.clearTimeout(timer)
1470
1659
  }, [hasLoadedInitialScene, isLoading, isSceneLoading, isViewerSceneReady, sceneReadyKey])
1471
1660
 
1472
- const showLoader = isLoading || isSceneLoading || !hasLoadedInitialScene || !isViewerSceneReady
1661
+ const isSceneDataLoading = isLoading || isSceneLoading || !hasLoadedInitialScene
1662
+ const showLoader = isSceneDataLoading || !isViewerSceneReady
1473
1663
  const visibleLoader =
1474
1664
  showLoader &&
1475
1665
  !(
@@ -1486,10 +1676,13 @@ function EditorContent({
1486
1676
 
1487
1677
  const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
1488
1678
  const wasFirstPersonModeRef = useRef(isFirstPersonMode)
1679
+ const firstPersonWasPreviewRef = useRef(isPreviewMode)
1489
1680
 
1490
1681
  useEffect(() => {
1491
1682
  const wasFirstPersonMode = wasFirstPersonModeRef.current
1683
+ const wasPreviewMode = firstPersonWasPreviewRef.current
1492
1684
  wasFirstPersonModeRef.current = isFirstPersonMode
1685
+ firstPersonWasPreviewRef.current = isPreviewMode
1493
1686
 
1494
1687
  if (isFirstPersonMode && !wasFirstPersonMode) {
1495
1688
  const viewer = useViewer.getState()
@@ -1508,7 +1701,8 @@ function EditorContent({
1508
1701
  firstPersonPreviousLevelRef.current = null
1509
1702
  viewer.setWalkthroughMode(false)
1510
1703
 
1511
- if (!previousLevelId) return
1704
+ // Leaving Preview has already restored the editor's complete selection.
1705
+ if ((wasPreviewMode && !isPreviewMode) || !previousLevelId) return
1512
1706
 
1513
1707
  const previousLevelNode = useScene.getState().nodes[previousLevelId]
1514
1708
  if (previousLevelNode?.type === 'level') {
@@ -1518,39 +1712,56 @@ function EditorContent({
1518
1712
  selectedIds: [],
1519
1713
  })
1520
1714
  }
1521
- }, [isFirstPersonMode])
1715
+ }, [isFirstPersonMode, isPreviewMode])
1522
1716
 
1523
1717
  const previewViewerContent = (
1524
1718
  <Viewer
1525
1719
  defaultRender={EDITOR_DEFAULT_RENDER}
1526
1720
  disablePostFx={disablePostFx}
1527
1721
  hoverStyles={EDITOR_HOVER_STYLES}
1722
+ onBreadcrumb={onViewerBreadcrumb}
1528
1723
  renderContext="editor"
1529
- selectionManager="default"
1724
+ selectionManager="custom"
1530
1725
  >
1531
1726
  <ExportManager />
1532
- <ViewerZoneSystem />
1727
+ <ViewerSelectionManager />
1728
+ <ParametricViewerRooms />
1533
1729
  <CeilingSystem />
1534
1730
  <RoofEditSystem />
1535
1731
  <StairEditSystem />
1536
1732
  {isFirstPersonMode && <FirstPersonControls />}
1537
1733
  <CustomCameraControls />
1538
1734
  <ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
1735
+ <VectorEdgeExtractor />
1539
1736
  <InteractiveSystem />
1540
1737
  {presentationsReady ? <ViewerPresentations /> : null}
1541
1738
  </Viewer>
1542
1739
  )
1543
1740
 
1741
+ const loadingOverlay = showLoader ? (
1742
+ <div className="absolute inset-0 z-50" data-scene-loading>
1743
+ {sceneLoadError ? (
1744
+ <SceneLoadFailed className="bg-background" onRetry={retrySceneLoad} />
1745
+ ) : (
1746
+ (loadingSlot ?? <SceneLoader className="bg-background" />)
1747
+ )}
1748
+ </div>
1749
+ ) : null
1750
+
1544
1751
  const viewerCanvas = (
1545
1752
  <ViewerCanvas
1546
1753
  disablePostFx={disablePostFx}
1754
+ hintBelowTopMenu={topMenuShown}
1547
1755
  hasLoadedInitialScene={hasLoadedInitialScene}
1548
1756
  isFirstPersonMode={isFirstPersonMode}
1549
1757
  isLoading={isLoading}
1758
+ isSceneDataLoading={isSceneDataLoading}
1550
1759
  isStudioMode={isStudioMode}
1551
1760
  isVersionPreviewMode={isVersionPreviewMode}
1761
+ loadingOverlay={loadingOverlay}
1552
1762
  onSceneReadyChange={handleSceneReadyChange}
1553
1763
  onThumbnailCapture={onThumbnailCapture}
1764
+ onViewerBreadcrumb={onViewerBreadcrumb}
1554
1765
  presentationsReady={presentationsReady}
1555
1766
  sceneReadyKey={sceneReadyKey}
1556
1767
  showLoader={showLoader}
@@ -1605,27 +1816,23 @@ function EditorContent({
1605
1816
  mobileDefaultSnap: 0.5,
1606
1817
  mobileIcon: p.icon,
1607
1818
  icon: p.icon,
1819
+ noPanel: p.noPanel,
1820
+ onSelect: p.onSelect,
1821
+ stageActive: p.stageActive,
1822
+ onDeselect: p.onDeselect,
1608
1823
  })),
1609
1824
  ]
1610
1825
 
1611
1826
  return (
1612
1827
  <>
1613
1828
  <FloorplanModeCoordinator />
1614
- {visibleLoader && (
1615
- <div className="fixed inset-0 z-60">
1616
- {sceneLoadError ? (
1617
- <SceneLoadFailed className="bg-background" onRetry={retrySceneLoad} />
1618
- ) : (
1619
- <SceneLoader className="bg-background" />
1620
- )}
1621
- </div>
1622
- )}
1623
-
1624
1829
  {!isLoading && isPreviewMode ? (
1625
1830
  <PreviewStage
1626
1831
  isFirstPersonMode={isFirstPersonMode}
1832
+ loadingOverlay={loadingOverlay}
1627
1833
  mode={previewStageMode}
1628
1834
  onModeChange={setPreviewStageMode}
1835
+ projectName={projectName}
1629
1836
  showLoader={visibleLoader}
1630
1837
  viewerContent={previewViewerContent}
1631
1838
  />
@@ -1635,25 +1842,38 @@ function EditorContent({
1635
1842
  navbarSlot={navbarSlot}
1636
1843
  overlays={
1637
1844
  <>
1638
- {!(isCaptureMode || stageOverlay) && <FloatingLevelSelector />}
1639
- {!(isVersionPreviewMode || isCaptureMode || isStudioMode) && (
1640
- <div className="pointer-events-auto">
1641
- <ActionMenu />
1642
- </div>
1643
- )}
1644
- {!(isVersionPreviewMode || isCaptureMode || isStudioMode) && (
1645
- <div className="pointer-events-auto">
1646
- <PanelManager
1647
- inspectorFooter={inspectorFooter}
1648
- multiSelectionFooter={multiSelectionFooter}
1649
- />
1650
- </div>
1651
- )}
1652
- {!isCaptureMode && (
1653
- <div className="pointer-events-auto">
1654
- <HelperManager />
1655
- </div>
1656
- )}
1845
+ <SceneRegion>
1846
+ {!(isCaptureMode || stageOverlay) && <FloatingLevelSelector />}
1847
+ {!(isVersionPreviewMode || isCaptureMode || isStudioMode) &&
1848
+ (dockMenuOnTop ? (
1849
+ // Own row under the toolbar, below the level selector's opening list (z-20).
1850
+ // No transform: the menu's fixed pill must anchor to the scene region.
1851
+ <div className="pointer-events-none absolute inset-x-0 top-14 z-10 flex justify-center">
1852
+ <div className="pointer-events-auto">
1853
+ <ActionMenu inline placement="top" />
1854
+ </div>
1855
+ </div>
1856
+ ) : (
1857
+ <div className="pointer-events-auto">
1858
+ <ActionMenu placement={actionMenuPlacement} />
1859
+ </div>
1860
+ ))}
1861
+ {sceneOverlay}
1862
+ </SceneRegion>
1863
+ {/* The inspector and the shortcuts card share one right column. */}
1864
+ <RightStack
1865
+ belowTopMenu={topMenuShown}
1866
+ reserveBottomMenu={!dockMenuOnTop}
1867
+ helper={isCaptureMode ? null : <HelperManager />}
1868
+ inspector={
1869
+ isVersionPreviewMode || isCaptureMode || isStudioMode ? null : (
1870
+ <PanelManager
1871
+ inspectorFooter={inspectorFooter}
1872
+ multiSelectionFooter={multiSelectionFooter}
1873
+ />
1874
+ )
1875
+ }
1876
+ />
1657
1877
  {/* Capture mode drives walk / drone from its own overlay, which
1658
1878
  owns the framing chrome — the walkthrough HUD would both
1659
1879
  clutter the frame and offer a second, conflicting exit. */}
@@ -1670,6 +1890,7 @@ function EditorContent({
1670
1890
  sidebarOverlay={sidebarOverlay}
1671
1891
  sidebarTabs={tabBarTabs}
1672
1892
  stageOverlay={stageOverlay}
1893
+ viewBar={viewBar}
1673
1894
  viewerContent={viewerCanvas}
1674
1895
  viewerToolbarLeft={viewerToolbarLeft}
1675
1896
  viewerToolbarRight={viewerToolbarRight}
@@ -1689,23 +1910,15 @@ function EditorContent({
1689
1910
  const overlayLeft = LAYOUT_PADDING + (isSidebarCollapsed ? 8 : sidebarWidth) + LAYOUT_GAP
1690
1911
 
1691
1912
  return (
1692
- <div className="dark flex h-full w-full gap-3 bg-neutral-100 p-3 text-foreground">
1913
+ <div className="flex h-full w-full gap-3 bg-neutral-100 p-3 text-foreground">
1693
1914
  <FloorplanModeCoordinator />
1694
- {visibleLoader && (
1695
- <div className="fixed inset-0 z-60">
1696
- {sceneLoadError ? (
1697
- <SceneLoadFailed className="bg-background" onRetry={retrySceneLoad} />
1698
- ) : (
1699
- <SceneLoader className="bg-background" />
1700
- )}
1701
- </div>
1702
- )}
1703
-
1704
1915
  {!isLoading && isPreviewMode ? (
1705
1916
  <PreviewStage
1706
1917
  isFirstPersonMode={isFirstPersonMode}
1918
+ loadingOverlay={loadingOverlay}
1707
1919
  mode={previewStageMode}
1708
1920
  onModeChange={setPreviewStageMode}
1921
+ projectName={projectName}
1709
1922
  showLoader={visibleLoader}
1710
1923
  viewerContent={previewViewerContent}
1711
1924
  />
@@ -1729,14 +1942,13 @@ function EditorContent({
1729
1942
  {/* Fixed UI overlays scoped to the viewer area */}
1730
1943
  <ViewerOverlays left={overlayLeft}>
1731
1944
  <div className="pointer-events-auto">
1732
- <ActionMenu />
1733
- </div>
1734
- <div className="pointer-events-auto">
1735
- <PanelManager />
1736
- </div>
1737
- <div className="pointer-events-auto">
1738
- <HelperManager />
1945
+ <ActionMenu placement={actionMenuPlacement} />
1739
1946
  </div>
1947
+ <RightStack
1948
+ helper={<HelperManager />}
1949
+ inspector={<PanelManager />}
1950
+ reserveBottomMenu={actionMenuPlacement !== 'top' || isMobile}
1951
+ />
1740
1952
  <RiserDiagramPanel />
1741
1953
  {isFirstPersonMode && (
1742
1954
  <FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />