@pascal-app/editor 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -19,9 +19,11 @@ import {
19
19
  SceneEnvironment,
20
20
  useViewer,
21
21
  Viewer,
22
+ type ViewerImmersiveSession,
22
23
  ViewerPresentations,
23
24
  } from '@pascal-app/viewer'
24
25
  import {
26
+ type CSSProperties,
25
27
  memo,
26
28
  Profiler,
27
29
  type ProfilerOnRenderCallback,
@@ -37,11 +39,17 @@ import { ViewerZoneSystem } from '../../components/viewer-zone-system'
37
39
  import { type SaveStatus, useAutoSave } from '../../hooks/use-auto-save'
38
40
  import { useKeyboard } from '../../hooks/use-keyboard'
39
41
  import { useSaveShortcut } from '../../hooks/use-save-shortcut'
42
+ import { useCeilingEditSessionOwner } from '../../lib/ceiling-edit-session'
43
+ import { showsWholeBuilding, useEditorLevelDisplay } from '../../lib/editor-level-display'
44
+ import { isSceneView } from '../../lib/editor-views'
45
+ import { useGestureLifecycleOwner } from '../../lib/gesture-lifecycle'
40
46
  import {
41
47
  createLocalProjectPresentationPersistence,
42
48
  type LocalProjectPresentationPersistence,
43
49
  } from '../../lib/local-project-presentation-persistence'
44
50
  import { type ActivePaintMaterial, hasActivePaintMaterial } from '../../lib/material-paint'
51
+ import { usePaintRegionHovering } from '../../lib/paint-region-hover'
52
+ import { resetPaintMode, usePaintRegionMode } from '../../lib/paint-region-mode'
45
53
  import {
46
54
  applySceneGraphToEditor,
47
55
  loadSceneFromLocalStorage,
@@ -50,16 +58,17 @@ import {
50
58
  } from '../../lib/scene'
51
59
  import { disposeSFXBus, initSFXBus } from '../../lib/sfx-bus'
52
60
  import { useUnitFocusRules } from '../../lib/units'
61
+ import { useViewerFloorDisplay } from '../../lib/viewer-selection'
53
62
  import { type CameraHintAction, useCameraHintFocus } from '../../store/use-camera-hint-focus'
54
63
  import useEditor from '../../store/use-editor'
55
64
  import useFloorplanMode from '../../store/use-floorplan-mode'
56
65
  import useSessionGroups from '../../store/use-session-groups'
66
+ import { paneOfView, VIEW_2D, VIEW_3D, type ViewPaneIndex } from '../../store/view-layout'
57
67
  import { CeilingSelectionAffordanceSystem } from '../systems/ceiling/ceiling-selection-affordance-system'
58
68
  import { CeilingSystem } from '../systems/ceiling/ceiling-system'
59
69
  import { RoofEditSystem } from '../systems/roof/roof-edit-system'
60
70
  import { SelectionAffordanceManager } from '../systems/selection-affordance-manager'
61
71
  import { StairEditSystem } from '../systems/stair/stair-edit-system'
62
- import { ZoneLabelEditorSystem } from '../systems/zone/zone-label-editor-system'
63
72
  import { ZoneSystem } from '../systems/zone/zone-system'
64
73
  import { BoxSelectTool } from '../tools/select/box-select-tool'
65
74
  import { ToolManager } from '../tools/tool-manager'
@@ -72,6 +81,7 @@ import { PanelManager } from '../ui/panels/panel-manager'
72
81
  import { ErrorBoundary } from '../ui/primitives/error-boundary'
73
82
  import { useSidebarStore } from '../ui/primitives/sidebar'
74
83
  import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/primitives/tooltip'
84
+ import { RightStack } from '../ui/right-stack'
75
85
  import { SceneLoader, SceneLoadFailed } from '../ui/scene-loader'
76
86
  import { AppSidebar } from '../ui/sidebar/app-sidebar'
77
87
  import type { ExtraPanel } from '../ui/sidebar/icon-rail'
@@ -79,6 +89,9 @@ import { SettingsPanel, type SettingsPanelProps } from '../ui/sidebar/panels/set
79
89
  import { SitePanel, type SitePanelProps } from '../ui/sidebar/panels/site-panel'
80
90
  import type { SidebarTab } from '../ui/sidebar/tab-bar'
81
91
  import { useHostPanels } from '../ui/sidebar/use-plugin-panels'
92
+ import { ViewerHoverLabel } from '../viewer/viewer-hover-label'
93
+ import { ParametricViewerRooms } from '../viewer/viewer-rooms'
94
+ import { ViewerSelectionManager } from '../viewer/viewer-selection-manager'
82
95
  import { ViewerStage } from '../viewer/viewer-stage'
83
96
  import type { ViewerStageMode } from '../viewer/viewer-stage-modes'
84
97
  import { CaptureCameraRig } from './capture-camera-rig'
@@ -92,21 +105,29 @@ import { FloatingActionMenu } from './floating-action-menu'
92
105
  import { FloatingBuildingActionMenu } from './floating-building-action-menu'
93
106
  import { FloorplanModeCoordinator } from './floorplan-mode-coordinator'
94
107
  import { FloorplanPanel } from './floorplan-panel'
108
+ import { FootprintHeightHandle } from './footprint-height-handle'
95
109
  import { Grid } from './grid'
96
110
  import { GroupFloatingActionMenu } from './group-floating-action-menu'
97
111
  import { GroupRotateHandle } from './group-rotate-handle'
98
112
  import { GroupSelectionBox3D } from './group-selection-box-3d'
113
+ import { EditorHandleHitPriority } from './handles/handle-hit-priority'
99
114
  import { NodeArrowHandles } from './node-arrow-handles'
100
115
  import { QuickMeasurementHud } from './quick-measurement-hud'
101
116
  import { RiserDiagramPanel } from './riser-diagram-panel'
117
+ import { RoomLabels3D } from './room-labels'
102
118
  import { SelectionManager } from './selection-manager'
103
119
  import { SiteEdgeLabels } from './site-edge-labels'
104
120
  import { SlabHoleHighlights } from './slab-hole-highlights'
105
121
  import { SnapshotCaptureOverlay } from './snapshot-capture-overlay'
106
122
  import { type SnapshotCameraData, ThumbnailGenerator } from './thumbnail-generator'
123
+ import { VectorEdgeExtractor } from './vector-edge-extractor'
124
+ import { HostView } from './views/host-view'
125
+ import { SceneRegion } from './views/scene-region'
126
+ import { useActiveViewLayout, useEditorViews } from './views/use-editor-views'
107
127
  import { WallMeasurementLabel } from './wall-measurement-label'
108
128
  import { WallMoveSideHandles } from './wall-move-side-handles'
109
129
  import { WallOpeningHighlights } from './wall-opening-highlights'
130
+ import { WallRegionHandles } from './wall-region-handles'
110
131
 
111
132
  const CAMERA_CONTROLS_HINT_DISMISSED_STORAGE_KEY = 'editor-camera-controls-hint-dismissed:v1'
112
133
  const PREVIEW_STAGE_SWITCHER_POSITION =
@@ -116,12 +137,16 @@ const DELETE_CURSOR_BADGE_OFFSET_X = 14
116
137
  const DELETE_CURSOR_BADGE_OFFSET_Y = 14
117
138
  const PAINT_CURSOR_BADGE_COLOR = '#818cf8'
118
139
  const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
140
+ // Neutral: erasing paints no colour.
141
+ const ERASE_CURSOR_BADGE_COLOR = '#e4e4e7'
119
142
  const PAINT_CURSOR_BADGE_OFFSET_X = 14
120
143
  const PAINT_CURSOR_BADGE_OFFSET_Y = 14
121
144
  const SCENE_READY_FALLBACK_MS = 8000
122
145
  const PRESENTATION_PROJECT_NOT_RESTORED = Symbol('presentation-project-not-restored')
123
146
  const useClientLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect
124
- type PaintCursorBadgeState = 'empty' | 'ready' | 'blocked'
147
+ // `aim`: a region sub-mode off what it draws on — the HUD says where to go; no
148
+ // forbidden sign, since nothing is refused.
149
+ type PaintCursorBadgeState = 'empty' | 'ready' | 'aim' | 'blocked'
125
150
  const recordEditorRender: ProfilerOnRenderCallback = (_id, _phase, actualDuration) => {
126
151
  if (PERF_OVERLAY_ENABLED) recordPerfSample('react-render', actualDuration)
127
152
  }
@@ -129,6 +154,8 @@ const EDITOR_HOVER_STYLES: HoverStyles = {
129
154
  default: { visibleColor: 0x00_aa_ff, hiddenColor: 0xf3_ff_47, strength: 5, pulse: true },
130
155
  delete: { visibleColor: 0xef_44_44, hiddenColor: 0x99_1b_1b, strength: 6, pulse: false },
131
156
  'paint-ready': { visibleColor: 0xf5_9e_0b, hiddenColor: 0xfd_e0_68, strength: 5, pulse: true },
157
+ // Erasing paints nothing: a neutral outline, no paint colour.
158
+ 'erase-ready': { visibleColor: 0xe4_e4_e7, hiddenColor: 0xa1_a1_aa, strength: 5, pulse: false },
132
159
  'paint-disabled': {
133
160
  visibleColor: 0x94_a3_b8,
134
161
  hiddenColor: 0x47_55_69,
@@ -176,11 +203,20 @@ export interface EditorProps {
176
203
  viewerToolbarLeft?: ReactNode
177
204
  viewerToolbarRight?: ReactNode
178
205
  /**
179
- * Full-bleed surface swapped in over the 3D canvas (v2) — e.g. the studio
180
- * gallery. The canvas stays mounted underneath (no WebGL re-init) and the
181
- * viewer toolbar stays on top so the host's stage switch remains reachable.
206
+ * Full-bleed surface swapped in over the whole stage (v2) — e.g. a builder
207
+ * with its own canvas. The scene stays mounted underneath (no WebGL re-init)
208
+ * and the view bar steps aside. Something to show *beside* the scene is a
209
+ * view instead (`registerEditorHostView`).
182
210
  */
183
211
  stageOverlay?: ReactNode
212
+ /**
213
+ * Chrome that belongs to the scene (v2), positioned over the 3D and 2D panes
214
+ * with the level selector and tool dock — e.g. a capture bar. Hidden while
215
+ * no scene view is on screen.
216
+ */
217
+ sceneOverlay?: ReactNode
218
+ /** Show each pane's view bar (v2, default true). Off for a host that presents one fixed view. */
219
+ viewBar?: boolean
184
220
  /**
185
221
  * Docked below the node inspector (v2). Hosts mount the "save as preset"
186
222
  * affordance here so it reads as part of the inspector surface and shows
@@ -202,6 +238,7 @@ export interface EditorProps {
202
238
  projectId?: string | null
203
239
 
204
240
  // Persistence — defaults to localStorage when omitted
241
+ guardAgainstSceneWipe?: boolean
205
242
  onLoad?: () => Promise<SceneGraph | null>
206
243
  onSave?: (scene: SceneGraph, options?: { keepalive?: boolean }) => Promise<void>
207
244
  /**
@@ -234,10 +271,16 @@ export interface EditorProps {
234
271
  */
235
272
  disablePostFx?: boolean
236
273
 
274
+ /** Host-provided immersive XR runtime for the main 3D canvas. */
275
+ immersive?: ViewerImmersiveSession
276
+
237
277
  // Version preview overlays (rendered by host app)
238
278
  sidebarOverlay?: ReactNode
239
279
  viewerBanner?: ReactNode
240
280
 
281
+ /** Shown in the preview header. */
282
+ projectName?: string | null
283
+
241
284
  // Panel config (passed through to sidebar panels — v1 only)
242
285
  settingsPanelProps?: SettingsPanelProps
243
286
  sitePanelProps?: SitePanelProps
@@ -417,7 +460,7 @@ type CameraControlHint = {
417
460
  alternativeKeys?: ShortcutKey[]
418
461
  }
419
462
 
420
- const EDITOR_CAMERA_CONTROL_HINTS: CameraControlHint[] = [
463
+ const CAMERA_CONTROL_HINTS: CameraControlHint[] = [
421
464
  {
422
465
  action: 'Pan',
423
466
  keys: [{ value: 'Space' }, { value: 'Left click' }],
@@ -427,12 +470,6 @@ const EDITOR_CAMERA_CONTROL_HINTS: CameraControlHint[] = [
427
470
  { action: 'Zoom', keys: [{ value: 'Scroll' }] },
428
471
  ]
429
472
 
430
- const PREVIEW_CAMERA_CONTROL_HINTS: CameraControlHint[] = [
431
- { action: 'Pan', keys: [{ value: 'Left click' }] },
432
- { action: 'Rotate', keys: [{ value: 'Right click' }] },
433
- { action: 'Zoom', keys: [{ value: 'Scroll' }] },
434
- ]
435
-
436
473
  const CAMERA_SHORTCUT_KEY_META: Record<string, { icon?: string; label: string; text?: string }> = {
437
474
  'Left click': {
438
475
  icon: 'ph:mouse-left-click-fill',
@@ -539,14 +576,8 @@ function CameraControlHintItem({ hint }: { hint: CameraControlHint }) {
539
576
  )
540
577
  }
541
578
 
542
- function ViewerCanvasControlsHint({
543
- isPreviewMode,
544
- onDismiss,
545
- }: {
546
- isPreviewMode: boolean
547
- onDismiss: () => void
548
- }) {
549
- const all = isPreviewMode ? PREVIEW_CAMERA_CONTROL_HINTS : EDITOR_CAMERA_CONTROL_HINTS
579
+ function ViewerCanvasControlsHint({ onDismiss }: { onDismiss: () => void }) {
580
+ const all = CAMERA_CONTROL_HINTS
550
581
  // A host teaching one gesture at a time narrows this to the one it is asking
551
582
  // for, and to nothing once it is done. Null — the default — is all of them.
552
583
  const focus = useCameraHintFocus((state) => state.actions)
@@ -675,26 +706,31 @@ function PaintCursorBadge({
675
706
  swatchColor,
676
707
  swatchImageUrl,
677
708
  isEraser,
709
+ isPicker = false,
678
710
  }: {
679
711
  position: { x: number; y: number }
680
712
  state: PaintCursorBadgeState
681
713
  swatchColor: string
682
714
  swatchImageUrl?: string
683
715
  isEraser: boolean
716
+ /** The eyedropper: the swatch is what a click would take. */
717
+ isPicker?: boolean
684
718
  }) {
685
719
  const accentColor =
686
720
  state === 'ready'
687
721
  ? isEraser
688
- ? PAINT_CURSOR_BADGE_COLOR
722
+ ? ERASE_CURSOR_BADGE_COLOR
689
723
  : swatchColor
690
724
  : PAINT_CURSOR_BADGE_DISABLED_COLOR
691
- const iconOpacity = state === 'ready' ? 1 : state === 'blocked' ? 0.62 : 0.42
725
+ const iconOpacity = state === 'ready' ? 1 : state === 'blocked' || state === 'aim' ? 0.62 : 0.42
692
726
  const lineHeight = 18
693
727
 
694
728
  return (
695
729
  <div
696
730
  aria-hidden="true"
697
731
  className="pointer-events-none absolute z-40"
732
+ data-paint-cursor-state={state}
733
+ data-paint-cursor-tool={isEraser ? 'erase' : isPicker ? 'pick' : 'paint'}
698
734
  style={{
699
735
  left: position.x,
700
736
  top: position.y,
@@ -716,29 +752,32 @@ function PaintCursorBadge({
716
752
  transform: `translate(-50%, calc(-100% - ${lineHeight}px))`,
717
753
  }}
718
754
  >
719
- {/* eslint-disable-next-line @next/next/no-img-element */}
720
- <img
721
- alt=""
722
- aria-hidden="true"
723
- className="h-5 w-5 object-contain drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
724
- src="/icons/paint.webp"
725
- style={{
726
- filter: state === 'ready' ? undefined : 'grayscale(1)',
727
- opacity: iconOpacity,
728
- }}
729
- />
730
- {state === 'ready' ? (
731
- isEraser ? (
732
- <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)]">
733
- <Icon
734
- aria-hidden="true"
735
- color="currentColor"
736
- height={10}
737
- icon="mdi:eraser-variant"
738
- width={10}
739
- />
740
- </span>
741
- ) : (
755
+ {isPicker || isEraser ? (
756
+ // The eyedropper and the eraser carry their own tool, never a paint colour.
757
+ <Icon
758
+ aria-hidden="true"
759
+ className="text-white"
760
+ color="currentColor"
761
+ height={18}
762
+ icon={isEraser ? 'mdi:eraser-variant' : 'lucide:pipette'}
763
+ style={{ opacity: iconOpacity }}
764
+ width={18}
765
+ />
766
+ ) : (
767
+ /* eslint-disable-next-line @next/next/no-img-element */
768
+ <img
769
+ alt=""
770
+ aria-hidden="true"
771
+ className="h-5 w-5 object-contain drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
772
+ src="/icons/paint.webp"
773
+ style={{
774
+ filter: state === 'ready' ? undefined : 'grayscale(1)',
775
+ opacity: iconOpacity,
776
+ }}
777
+ />
778
+ )}
779
+ {state === 'ready' || state === 'aim' ? (
780
+ isEraser ? null : (
742
781
  <span
743
782
  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)]"
744
783
  style={{
@@ -746,6 +785,7 @@ function PaintCursorBadge({
746
785
  backgroundImage: swatchImageUrl
747
786
  ? `url(${JSON.stringify(swatchImageUrl)})`
748
787
  : undefined,
788
+ opacity: state === 'aim' ? 0.55 : 1,
749
789
  }}
750
790
  />
751
791
  )
@@ -783,6 +823,7 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
783
823
  isVersionPreviewMode,
784
824
  isLoading,
785
825
  isFirstPersonMode,
826
+ isXRMode,
786
827
  isStudioMode,
787
828
  onThumbnailCapture,
788
829
  viewerSceneSlot,
@@ -791,6 +832,7 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
791
832
  isVersionPreviewMode: boolean
792
833
  isLoading: boolean
793
834
  isFirstPersonMode: boolean
835
+ isXRMode: boolean
794
836
  isStudioMode: boolean
795
837
  onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
796
838
  viewerSceneSlot?: ReactNode
@@ -807,18 +849,22 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
807
849
  <>
808
850
  <SceneEnvironment />
809
851
  {!(isFirstPersonMode || isStudioMode || isCaptureMode) && <SelectionManager />}
810
- {!noEditing && <BoxSelectTool />}
852
+ {!(noEditing || isXRMode) && <BoxSelectTool />}
853
+ {!noEditing && <EditorHandleHitPriority />}
811
854
  {!noEditing && <NodeArrowHandles />}
855
+ {!noEditing && <FootprintHeightHandle />}
812
856
  {!noEditing && <GroupRotateHandle />}
813
857
  {!noEditing && <GroupSelectionBox3D />}
814
858
  {!noEditing && <WallOpeningHighlights />}
815
859
  {!noEditing && <SlabHoleHighlights />}
816
860
  {!noEditing && <WallMoveSideHandles />}
861
+ {!noEditing && <WallRegionHandles />}
817
862
  {!noEditing && <FenceTangentLines3D />}
818
- {!noEditing && <FloatingActionMenu />}
819
- {!noEditing && <GroupFloatingActionMenu />}
820
- {!noEditing && <FloatingBuildingActionMenu />}
821
- {!isFirstPersonMode && <WallMeasurementLabel />}
863
+ {!(noEditing || isXRMode) && <FloatingActionMenu />}
864
+ {!(noEditing || isXRMode) && <GroupFloatingActionMenu />}
865
+ {!(noEditing || isXRMode) && <FloatingBuildingActionMenu />}
866
+ {!(noEditing || isXRMode) && <RoomLabels3D />}
867
+ {!(isFirstPersonMode || isXRMode) && <WallMeasurementLabel />}
822
868
  <ExportManager />
823
869
  {isFirstPersonMode ? <ViewerZoneSystem /> : <ZoneSystem />}
824
870
  <CeilingSystem />
@@ -829,11 +875,11 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
829
875
  {!(isLoading || isFirstPersonMode) && <SnapAwareGrid />}
830
876
  {!(isLoading || noEditing) && <ToolManager />}
831
877
  {isFirstPersonMode && <FirstPersonControls />}
832
- {isCaptureMode && <CaptureCameraRig />}
833
- <CustomCameraControls />
834
- <ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
835
- {!isFirstPersonMode && <SiteEdgeLabels />}
836
- <InteractiveSystem />
878
+ {isCaptureMode && !isXRMode && <CaptureCameraRig />}
879
+ {!isXRMode && <CustomCameraControls />}
880
+ {!isXRMode && <ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />}
881
+ {!isXRMode && <VectorEdgeExtractor />}
882
+ {!(isFirstPersonMode || isXRMode) && <SiteEdgeLabels />}
837
883
  {presentationsReady ? <ViewerPresentations /> : null}
838
884
  {!noEditing && viewerSceneSlot}
839
885
  </>
@@ -926,7 +972,10 @@ function PaintCursorLayer({
926
972
  }) {
927
973
  const mode = useEditor((s) => s.mode)
928
974
  const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
929
- const paintEraser = useEditor((s) => s.paintEraser)
975
+ const paintMode = usePaintRegionMode((s) => s.mode)
976
+ const picked = usePaintRegionMode((s) => s.picked)
977
+ const paintEraser = paintMode === 'erase'
978
+ const picking = paintMode === 'pick'
930
979
  const paintHover = useEditor((s) => s.paintHover)
931
980
  const sceneMaterials = useScene((s) => s.materials)
932
981
  const [position, setPosition] = useState<{ x: number; y: number } | null>(null)
@@ -978,14 +1027,26 @@ function PaintCursorLayer({
978
1027
  }
979
1028
  }, [active, containerRef])
980
1029
 
981
- const hasPaint = paintEraser || hasActivePaintMaterial(activePaintMaterial)
1030
+ const hasPaint = paintEraser || picking || hasActivePaintMaterial(activePaintMaterial)
1031
+ // A region sub-mode reads its own surfaces (walls, floors), not the whole-surface hover.
1032
+ const regionMode = paintMode === 'rectangle' || paintMode === 'polygon'
1033
+ const regionHovering = usePaintRegionHovering()
982
1034
  const badgeState: PaintCursorBadgeState = !hasPaint
983
1035
  ? 'empty'
984
- : paintHover != null
985
- ? 'ready'
986
- : 'blocked'
987
- const swatchColor = getActivePaintMaterialSwatchColor(activePaintMaterial, sceneMaterials)
988
- const swatchImageUrl = getActivePaintMaterialSwatchImageUrl(activePaintMaterial, sceneMaterials)
1036
+ : picking
1037
+ ? picked
1038
+ ? 'ready'
1039
+ : 'aim'
1040
+ : regionMode
1041
+ ? regionHovering
1042
+ ? 'ready'
1043
+ : 'aim'
1044
+ : paintHover != null
1045
+ ? 'ready'
1046
+ : 'blocked'
1047
+ const swatchMaterial = picking ? picked : activePaintMaterial
1048
+ const swatchColor = getActivePaintMaterialSwatchColor(swatchMaterial, sceneMaterials)
1049
+ const swatchImageUrl = getActivePaintMaterialSwatchImageUrl(swatchMaterial, sceneMaterials)
989
1050
 
990
1051
  if (!active || !position) return null
991
1052
 
@@ -996,6 +1057,7 @@ function PaintCursorLayer({
996
1057
  >
997
1058
  <PaintCursorBadge
998
1059
  isEraser={paintEraser}
1060
+ isPicker={picking}
999
1061
  position={{ x: 0, y: 0 }}
1000
1062
  state={badgeState}
1001
1063
  swatchColor={swatchColor}
@@ -1005,8 +1067,8 @@ function PaintCursorLayer({
1005
1067
  )
1006
1068
  }
1007
1069
 
1008
- // ── Viewer canvas: memoized, subscribes to viewMode/floorplanPaneRatio internally ──
1009
- // This prevents Editor from re-rendering when those values change.
1070
+ // ── Viewer canvas: memoized, subscribes to the view layout internally ──
1071
+ // This prevents Editor from re-rendering when the layout changes.
1010
1072
 
1011
1073
  const ViewerCanvas = memo(function ViewerCanvas({
1012
1074
  isVersionPreviewMode,
@@ -1022,6 +1084,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
1022
1084
  viewerSceneSlot,
1023
1085
  floorplanSceneSlot,
1024
1086
  disablePostFx = false,
1087
+ immersive,
1025
1088
  }: {
1026
1089
  isVersionPreviewMode: boolean
1027
1090
  isLoading: boolean
@@ -1036,13 +1099,22 @@ const ViewerCanvas = memo(function ViewerCanvas({
1036
1099
  viewerSceneSlot?: ReactNode
1037
1100
  floorplanSceneSlot?: ReactNode
1038
1101
  disablePostFx?: boolean
1102
+ immersive?: ViewerImmersiveSession
1039
1103
  }) {
1040
- const viewMode = useEditor((s) => s.viewMode)
1041
- const floorplanPaneRatio = useEditor((s) => s.floorplanPaneRatio)
1042
- const setFloorplanPaneRatio = useEditor((s) => s.setFloorplanPaneRatio)
1104
+ const layout = useActiveViewLayout()
1105
+ const views = useEditorViews()
1043
1106
  const isPreviewMode = useEditor((s) => s.isPreviewMode)
1044
1107
  const isCaptureMode = useEditor((s) => s.isCaptureMode)
1045
1108
  useUnitFocusRules()
1109
+ const wholeBuilding = useEditor((s) =>
1110
+ showsWholeBuilding({
1111
+ isPreviewMode: s.isPreviewMode,
1112
+ isFirstPersonMode,
1113
+ captureMode: s.captureMode,
1114
+ captureLevelId: s.captureLevelId,
1115
+ }),
1116
+ )
1117
+ useEditorLevelDisplay(wholeBuilding)
1046
1118
  const presetIsolation = useEditor((s) =>
1047
1119
  s.captureMode.mode === 'preset' ? s.captureMode.isolated : null,
1048
1120
  )
@@ -1060,25 +1132,24 @@ const ViewerCanvas = memo(function ViewerCanvas({
1060
1132
  setViewerAreaEl(el)
1061
1133
  }, [])
1062
1134
  const viewer3dRef = useRef<HTMLDivElement>(null)
1063
- const isResizingFloorplan = useRef(false)
1135
+ const isResizingPanes = useRef(false)
1064
1136
 
1065
- const handleFloorplanDividerDown = useCallback((e: React.PointerEvent) => {
1137
+ const handlePaneDividerDown = useCallback((e: React.PointerEvent) => {
1066
1138
  e.preventDefault()
1067
- isResizingFloorplan.current = true
1139
+ isResizingPanes.current = true
1068
1140
  document.body.style.cursor = 'col-resize'
1069
1141
  document.body.style.userSelect = 'none'
1070
1142
  }, [])
1071
1143
 
1072
1144
  useEffect(() => {
1073
1145
  const handlePointerMove = (e: PointerEvent) => {
1074
- if (!isResizingFloorplan.current) return
1146
+ if (!isResizingPanes.current) return
1075
1147
  if (!viewerAreaRef.current) return
1076
1148
  const rect = viewerAreaRef.current.getBoundingClientRect()
1077
- const newRatio = (e.clientX - rect.left) / rect.width
1078
- setFloorplanPaneRatio(Math.max(0.15, Math.min(0.85, newRatio)))
1149
+ useEditor.getState().setViewPaneRatio((e.clientX - rect.left) / rect.width)
1079
1150
  }
1080
1151
  const handlePointerUp = () => {
1081
- isResizingFloorplan.current = false
1152
+ isResizingPanes.current = false
1082
1153
  document.body.style.cursor = ''
1083
1154
  document.body.style.userSelect = ''
1084
1155
  }
@@ -1088,7 +1159,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
1088
1159
  window.removeEventListener('pointermove', handlePointerMove)
1089
1160
  window.removeEventListener('pointerup', handlePointerUp)
1090
1161
  }
1091
- }, [setFloorplanPaneRatio])
1162
+ }, [])
1092
1163
 
1093
1164
  useEffect(() => {
1094
1165
  setIsCameraControlsHintVisible(!readCameraControlsHintDismissed())
@@ -1099,89 +1170,128 @@ const ViewerCanvas = memo(function ViewerCanvas({
1099
1170
  writeCameraControlsHintDismissed(true)
1100
1171
  }, [])
1101
1172
 
1102
- const show2d = viewMode === '2d' || viewMode === 'split'
1103
- const show3d = viewMode === '3d' || viewMode === 'split'
1173
+ const pane2d = paneOfView(layout, VIEW_2D)
1174
+ const pane3d = paneOfView(layout, VIEW_3D)
1175
+ const show3d = pane3d !== null
1176
+ // Panes are grid cells placed by `gridColumn`, in a fixed DOM order: moving a
1177
+ // view to the other pane restyles it rather than reparenting it, so the 3D
1178
+ // canvas keeps its WebGL context and the plan its session through a swap.
1179
+ const paneStyle = (pane: ViewPaneIndex | null): CSSProperties =>
1180
+ pane === null ? { display: 'none' } : { gridColumn: pane + 1, gridRow: 1 }
1181
+ const hostPanes = ([0, 1] as const).filter(
1182
+ (pane) => (pane === 0 || layout.split) && !isSceneView(layout.panes[pane]),
1183
+ )
1184
+ const focusPane = (pane: ViewPaneIndex) => () => {
1185
+ if (layout.split) useEditor.getState().focusViewPane(pane)
1186
+ }
1104
1187
 
1105
1188
  return (
1106
1189
  <ErrorBoundary fallback={<EditorSceneCrashFallback />}>
1107
1190
  {/* `relative` so the floorplan compass (portaled here to stay visible in
1108
- 2d / 3d / split alike) can anchor to this container's bottom-left. */}
1109
- <div className="relative flex h-full" ref={setViewerAreaNode}>
1191
+ every layout) can anchor to this container's bottom-left. */}
1192
+ <div className="relative h-full" ref={setViewerAreaNode}>
1110
1193
  <QuickMeasurementHud />
1111
1194
  <DeleteConfirmationDialog />
1112
- {/* 2D floorplan — always mounted once shown, hidden via CSS to preserve state */}
1113
1195
  <div
1114
- className="relative h-full flex-shrink-0"
1196
+ className="absolute inset-0 grid"
1197
+ data-view-split={layout.split || undefined}
1115
1198
  style={{
1116
- width: viewMode === '2d' ? '100%' : `${floorplanPaneRatio * 100}%`,
1117
- display: show2d ? undefined : 'none',
1199
+ gridTemplateColumns: layout.split
1200
+ ? `minmax(0, ${layout.ratio}fr) minmax(0, ${1 - layout.ratio}fr)`
1201
+ : 'minmax(0, 1fr)',
1202
+ gridTemplateRows: 'minmax(0, 1fr)',
1118
1203
  }}
1119
1204
  >
1120
- <div className="h-full w-full overflow-hidden">
1121
- <FloorplanPanel compassHost={viewerAreaEl} floorplanSceneSlot={floorplanSceneSlot} />
1122
- </div>
1123
- {viewMode === 'split' && (
1124
- <div
1125
- className="absolute inset-y-0 -right-3 z-10 flex w-6 cursor-col-resize items-center justify-center"
1126
- onPointerDown={handleFloorplanDividerDown}
1127
- >
1128
- <div className="h-8 w-1 rounded-full bg-neutral-400" />
1205
+ {/* 2D floorplan — always mounted once shown, hidden via CSS to preserve state */}
1206
+ <div
1207
+ className="relative min-w-0 overflow-hidden"
1208
+ data-view-pane={pane2d ?? undefined}
1209
+ onPointerDownCapture={pane2d === null ? undefined : focusPane(pane2d)}
1210
+ style={paneStyle(pane2d)}
1211
+ >
1212
+ <div className="h-full w-full overflow-hidden">
1213
+ <FloorplanPanel compassHost={viewerAreaEl} floorplanSceneSlot={floorplanSceneSlot} />
1129
1214
  </div>
1130
- )}
1131
- </div>
1215
+ </div>
1132
1216
 
1133
- {/* 3D viewer — always mounted, hidden via CSS to avoid destroying the WebGL context */}
1134
- <div
1135
- className="relative min-w-0 flex-1 overflow-hidden"
1136
- data-pascal-viewer-3d
1137
- ref={viewer3dRef}
1138
- style={{ display: show3d ? undefined : 'none' }}
1139
- >
1140
- <DeleteCursorLayer
1141
- containerRef={viewer3dRef}
1142
- isVersionPreviewMode={isVersionPreviewMode}
1143
- />
1144
- <PaintCursorLayer
1145
- containerRef={viewer3dRef}
1146
- isVersionPreviewMode={isVersionPreviewMode}
1147
- />
1148
- {!showLoader && isCameraControlsHintVisible && !isFirstPersonMode ? (
1149
- <ViewerCanvasControlsHint
1150
- isPreviewMode={isPreviewMode}
1151
- onDismiss={dismissCameraControlsHint}
1152
- />
1153
- ) : null}
1154
- <SelectionPersistenceManager enabled={hasLoadedInitialScene && !showLoader} />
1155
- <Viewer
1156
- defaultRender={EDITOR_DEFAULT_RENDER}
1157
- disablePostFx={disablePostFx}
1158
- hoverStyles={EDITOR_HOVER_STYLES}
1159
- isolate={presetIsolation}
1160
- // Preset captures isolate one subtree and keep the exterior transparent.
1161
- // Other modes retain the viewer's configured background policy.
1162
- transparent={presetIsolation === null ? undefined : true}
1163
- onSceneReadyChange={onSceneReadyChange}
1164
- renderContext="editor"
1165
- renderPaused={!show3d && !showLoader}
1166
- sceneReadyKey={sceneReadyKey}
1167
- // Walk/drone framing during snapshot capture is camera-only: the
1168
- // viewer's default selection manager would hover-highlight whatever
1169
- // the cursor crosses, which orbit capture never does.
1170
- selectionManager={isFirstPersonMode && !isCaptureMode ? 'default' : 'custom'}
1217
+ {/* 3D viewer — always mounted, hidden via CSS to avoid destroying the WebGL context */}
1218
+ <div
1219
+ className="relative min-w-0 overflow-hidden"
1220
+ data-pascal-viewer-3d
1221
+ data-view-pane={pane3d ?? undefined}
1222
+ onPointerDownCapture={pane3d === null ? undefined : focusPane(pane3d)}
1223
+ ref={viewer3dRef}
1224
+ style={paneStyle(pane3d)}
1171
1225
  >
1172
- <ViewerSceneContent
1173
- isFirstPersonMode={isFirstPersonMode}
1174
- isLoading={showLoader}
1175
- isStudioMode={isStudioMode}
1226
+ <DeleteCursorLayer
1227
+ containerRef={viewer3dRef}
1176
1228
  isVersionPreviewMode={isVersionPreviewMode}
1177
- onThumbnailCapture={onThumbnailCapture}
1178
- presentationsReady={presentationsReady}
1179
- viewerSceneSlot={viewerSceneSlot}
1180
1229
  />
1181
- </Viewer>
1230
+ <PaintCursorLayer
1231
+ containerRef={viewer3dRef}
1232
+ isVersionPreviewMode={isVersionPreviewMode}
1233
+ />
1234
+ {!showLoader && isCameraControlsHintVisible && !isFirstPersonMode ? (
1235
+ <ViewerCanvasControlsHint onDismiss={dismissCameraControlsHint} />
1236
+ ) : null}
1237
+ <SelectionPersistenceManager enabled={hasLoadedInitialScene && !showLoader} />
1238
+ <Viewer
1239
+ defaultRender={EDITOR_DEFAULT_RENDER}
1240
+ disablePostFx={disablePostFx}
1241
+ hoverStyles={EDITOR_HOVER_STYLES}
1242
+ isolate={presetIsolation}
1243
+ // Preset captures isolate one subtree and keep the exterior transparent.
1244
+ // Other modes retain the viewer's configured background policy.
1245
+ transparent={presetIsolation === null ? undefined : true}
1246
+ onSceneReadyChange={onSceneReadyChange}
1247
+ renderContext="editor"
1248
+ renderPaused={!show3d && !showLoader}
1249
+ sceneReadyKey={sceneReadyKey}
1250
+ immersive={immersive}
1251
+ // Walk/drone framing during snapshot capture is camera-only: the
1252
+ // viewer's default selection manager would hover-highlight whatever
1253
+ // the cursor crosses, which orbit capture never does.
1254
+ selectionManager={isFirstPersonMode && !isCaptureMode ? 'default' : 'custom'}
1255
+ >
1256
+ <ViewerSceneContent
1257
+ isFirstPersonMode={isFirstPersonMode}
1258
+ isLoading={showLoader}
1259
+ isXRMode={immersive != null}
1260
+ isStudioMode={isStudioMode}
1261
+ isVersionPreviewMode={isVersionPreviewMode}
1262
+ onThumbnailCapture={onThumbnailCapture}
1263
+ presentationsReady={presentationsReady}
1264
+ viewerSceneSlot={viewerSceneSlot}
1265
+ />
1266
+ </Viewer>
1267
+ </div>
1268
+
1269
+ {hostPanes.map((pane) => {
1270
+ const viewId = layout.panes[pane]
1271
+ return (
1272
+ <div
1273
+ className="relative min-w-0 overflow-hidden"
1274
+ data-view-pane={pane}
1275
+ key={viewId}
1276
+ onPointerDownCapture={focusPane(pane)}
1277
+ style={paneStyle(pane)}
1278
+ >
1279
+ <HostView key={viewId} view={views.find((view) => view.id === viewId)} />
1280
+ </div>
1281
+ )
1282
+ })}
1182
1283
  </div>
1284
+
1285
+ {layout.split && (
1286
+ <div
1287
+ className="absolute inset-y-0 z-10 flex w-6 -translate-x-1/2 cursor-col-resize items-center justify-center"
1288
+ onPointerDown={handlePaneDividerDown}
1289
+ style={{ left: `${layout.ratio * 100}%` }}
1290
+ >
1291
+ <div className="h-8 w-1 rounded-full bg-neutral-400" />
1292
+ </div>
1293
+ )}
1183
1294
  </div>
1184
- {!(showLoader || isVersionPreviewMode) && <ZoneLabelEditorSystem />}
1185
1295
  </ErrorBoundary>
1186
1296
  )
1187
1297
  })
@@ -1190,12 +1300,14 @@ function PreviewStage({
1190
1300
  isFirstPersonMode,
1191
1301
  mode,
1192
1302
  onModeChange,
1303
+ projectName,
1193
1304
  showLoader,
1194
1305
  viewerContent,
1195
1306
  }: {
1196
1307
  isFirstPersonMode: boolean
1197
1308
  mode: ViewerStageMode
1198
1309
  onModeChange: (mode: ViewerStageMode) => void
1310
+ projectName?: string | null
1199
1311
  showLoader: boolean
1200
1312
  viewerContent: ReactNode
1201
1313
  }) {
@@ -1211,6 +1323,7 @@ function PreviewStage({
1211
1323
  [onModeChange],
1212
1324
  )
1213
1325
 
1326
+ useViewerFloorDisplay()
1214
1327
  const stageMode = isFirstPersonMode || !hasFloorplan ? '3d' : mode
1215
1328
  const stageModes = hasFloorplan && !isFirstPersonMode ? undefined : (['3d'] as const)
1216
1329
 
@@ -1222,9 +1335,11 @@ function PreviewStage({
1222
1335
  <ViewerOverlay
1223
1336
  hideBottomBar={stageMode !== '3d'}
1224
1337
  onBack={() => useEditor.getState().setPreviewMode(false)}
1338
+ projectName={projectName}
1225
1339
  />
1226
1340
  )}
1227
1341
 
1342
+ {stageMode === '3d' && !isFirstPersonMode ? <ViewerHoverLabel /> : null}
1228
1343
  <ViewerStage
1229
1344
  className="absolute inset-0"
1230
1345
  mode={stageMode}
@@ -1241,6 +1356,7 @@ function PreviewStage({
1241
1356
  }
1242
1357
 
1243
1358
  function EditorContent({
1359
+ guardAgainstSceneWipe,
1244
1360
  layoutVersion = 'v1',
1245
1361
  appMenuButton,
1246
1362
  sidebarTop,
@@ -1249,6 +1365,8 @@ function EditorContent({
1249
1365
  viewerToolbarLeft,
1250
1366
  viewerToolbarRight,
1251
1367
  stageOverlay,
1368
+ sceneOverlay,
1369
+ viewBar = true,
1252
1370
  inspectorFooter,
1253
1371
  multiSelectionFooter,
1254
1372
  viewerSceneSlot,
@@ -1265,8 +1383,10 @@ function EditorContent({
1265
1383
  onLoaderChange,
1266
1384
  onThumbnailCapture,
1267
1385
  disablePostFx = false,
1386
+ immersive,
1268
1387
  sidebarOverlay,
1269
1388
  viewerBanner,
1389
+ projectName,
1270
1390
  settingsPanelProps,
1271
1391
  sitePanelProps,
1272
1392
  extraSidebarPanels,
@@ -1298,8 +1418,10 @@ function EditorContent({
1298
1418
  }, [presentationProjectId])
1299
1419
 
1300
1420
  useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode || isStudioMode })
1421
+ useGestureLifecycleOwner()
1301
1422
 
1302
- const { isLoadingSceneRef, saveNow } = useAutoSave({
1423
+ const { beginSceneLoad, completeSceneLoad, saveNow } = useAutoSave({
1424
+ guardAgainstSceneWipe,
1303
1425
  onSave,
1304
1426
  onDirty,
1305
1427
  onSaveStatusChange,
@@ -1338,6 +1460,7 @@ function EditorContent({
1338
1460
  const teardown = initializeEditorRuntime()
1339
1461
  return teardown
1340
1462
  }, [])
1463
+ useCeilingEditSessionOwner()
1341
1464
 
1342
1465
  useEffect(() => {
1343
1466
  void useEditor.persist.rehydrate()
@@ -1347,6 +1470,7 @@ function EditorContent({
1347
1470
  useEffect(() => {
1348
1471
  useViewer.getState().setProjectId(projectId ?? null)
1349
1472
  useFloorplanMode.getState().setProjectId(projectId ?? null)
1473
+ resetPaintMode()
1350
1474
 
1351
1475
  return () => {
1352
1476
  useViewer.getState().setProjectId(null)
@@ -1359,7 +1483,7 @@ function EditorContent({
1359
1483
  let cancelled = false
1360
1484
 
1361
1485
  async function load(attempt: number) {
1362
- isLoadingSceneRef.current = true
1486
+ beginSceneLoad()
1363
1487
  setSceneLoadError(null)
1364
1488
  setHasLoadedInitialScene(false)
1365
1489
  setIsViewerSceneReady(false)
@@ -1374,6 +1498,9 @@ function EditorContent({
1374
1498
  const sceneGraph = onLoad ? await onLoad() : loadSceneFromLocalStorage()
1375
1499
  if (!cancelled && attempt === sceneLoadAttempt) {
1376
1500
  applySceneGraphToEditor(sceneGraph)
1501
+ // The store holds the loaded graph: autosave resumes now, not on a
1502
+ // frame — a tab loaded while hidden never gets one.
1503
+ completeSceneLoad()
1377
1504
  setIsViewerSceneReady(false)
1378
1505
  setSceneReadyKey((key) => key + 1)
1379
1506
  }
@@ -1390,9 +1517,11 @@ function EditorContent({
1390
1517
  setIsSceneLoading(false)
1391
1518
  if (!failed) {
1392
1519
  setHasLoadedInitialScene(true)
1393
- requestAnimationFrame(() => {
1394
- isLoadingSceneRef.current = false
1395
- })
1520
+ // A project opens in select mode. Rehydrate already drops the
1521
+ // persisted tool, but a host's rail panel may arm a mode of its
1522
+ // own when it mounts, and with a restored rail tab that mount can
1523
+ // land after rehydrate.
1524
+ useEditor.getState().armToolMode({ mode: 'select' })
1396
1525
  }
1397
1526
  }
1398
1527
  }
@@ -1403,7 +1532,7 @@ function EditorContent({
1403
1532
  return () => {
1404
1533
  cancelled = true
1405
1534
  }
1406
- }, [onLoad, isLoadingSceneRef, sceneLoadAttempt])
1535
+ }, [onLoad, beginSceneLoad, completeSceneLoad, sceneLoadAttempt])
1407
1536
 
1408
1537
  const retrySceneLoad = useCallback(() => {
1409
1538
  setSceneLoadAttempt((attempt) => attempt + 1)
@@ -1472,10 +1601,13 @@ function EditorContent({
1472
1601
 
1473
1602
  const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
1474
1603
  const wasFirstPersonModeRef = useRef(isFirstPersonMode)
1604
+ const firstPersonWasPreviewRef = useRef(isPreviewMode)
1475
1605
 
1476
1606
  useEffect(() => {
1477
1607
  const wasFirstPersonMode = wasFirstPersonModeRef.current
1608
+ const wasPreviewMode = firstPersonWasPreviewRef.current
1478
1609
  wasFirstPersonModeRef.current = isFirstPersonMode
1610
+ firstPersonWasPreviewRef.current = isPreviewMode
1479
1611
 
1480
1612
  if (isFirstPersonMode && !wasFirstPersonMode) {
1481
1613
  const viewer = useViewer.getState()
@@ -1494,7 +1626,8 @@ function EditorContent({
1494
1626
  firstPersonPreviousLevelRef.current = null
1495
1627
  viewer.setWalkthroughMode(false)
1496
1628
 
1497
- if (!previousLevelId) return
1629
+ // Leaving Preview has already restored the editor's complete selection.
1630
+ if ((wasPreviewMode && !isPreviewMode) || !previousLevelId) return
1498
1631
 
1499
1632
  const previousLevelNode = useScene.getState().nodes[previousLevelId]
1500
1633
  if (previousLevelNode?.type === 'level') {
@@ -1504,7 +1637,7 @@ function EditorContent({
1504
1637
  selectedIds: [],
1505
1638
  })
1506
1639
  }
1507
- }, [isFirstPersonMode])
1640
+ }, [isFirstPersonMode, isPreviewMode])
1508
1641
 
1509
1642
  const previewViewerContent = (
1510
1643
  <Viewer
@@ -1512,16 +1645,18 @@ function EditorContent({
1512
1645
  disablePostFx={disablePostFx}
1513
1646
  hoverStyles={EDITOR_HOVER_STYLES}
1514
1647
  renderContext="editor"
1515
- selectionManager="default"
1648
+ selectionManager="custom"
1516
1649
  >
1517
1650
  <ExportManager />
1518
- <ViewerZoneSystem />
1651
+ <ViewerSelectionManager />
1652
+ <ParametricViewerRooms />
1519
1653
  <CeilingSystem />
1520
1654
  <RoofEditSystem />
1521
1655
  <StairEditSystem />
1522
1656
  {isFirstPersonMode && <FirstPersonControls />}
1523
1657
  <CustomCameraControls />
1524
1658
  <ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
1659
+ <VectorEdgeExtractor />
1525
1660
  <InteractiveSystem />
1526
1661
  {presentationsReady ? <ViewerPresentations /> : null}
1527
1662
  </Viewer>
@@ -1542,6 +1677,7 @@ function EditorContent({
1542
1677
  showLoader={showLoader}
1543
1678
  viewerSceneSlot={viewerSceneSlot}
1544
1679
  floorplanSceneSlot={floorplanSceneSlot}
1680
+ immersive={immersive}
1545
1681
  />
1546
1682
  )
1547
1683
 
@@ -1590,6 +1726,10 @@ function EditorContent({
1590
1726
  mobileDefaultSnap: 0.5,
1591
1727
  mobileIcon: p.icon,
1592
1728
  icon: p.icon,
1729
+ noPanel: p.noPanel,
1730
+ onSelect: p.onSelect,
1731
+ stageActive: p.stageActive,
1732
+ onDeselect: p.onDeselect,
1593
1733
  })),
1594
1734
  ]
1595
1735
 
@@ -1611,6 +1751,7 @@ function EditorContent({
1611
1751
  isFirstPersonMode={isFirstPersonMode}
1612
1752
  mode={previewStageMode}
1613
1753
  onModeChange={setPreviewStageMode}
1754
+ projectName={projectName}
1614
1755
  showLoader={visibleLoader}
1615
1756
  viewerContent={previewViewerContent}
1616
1757
  />
@@ -1620,25 +1761,27 @@ function EditorContent({
1620
1761
  navbarSlot={navbarSlot}
1621
1762
  overlays={
1622
1763
  <>
1623
- {!(isCaptureMode || stageOverlay) && <FloatingLevelSelector />}
1624
- {!(isVersionPreviewMode || isCaptureMode || isStudioMode) && (
1625
- <div className="pointer-events-auto">
1626
- <ActionMenu />
1627
- </div>
1628
- )}
1629
- {!(isVersionPreviewMode || isCaptureMode || isStudioMode) && (
1630
- <div className="pointer-events-auto">
1631
- <PanelManager
1632
- inspectorFooter={inspectorFooter}
1633
- multiSelectionFooter={multiSelectionFooter}
1634
- />
1635
- </div>
1636
- )}
1637
- {!isCaptureMode && (
1638
- <div className="pointer-events-auto">
1639
- <HelperManager />
1640
- </div>
1641
- )}
1764
+ <SceneRegion>
1765
+ {!(isCaptureMode || stageOverlay) && <FloatingLevelSelector />}
1766
+ {!(isVersionPreviewMode || isCaptureMode || isStudioMode) && (
1767
+ <div className="pointer-events-auto">
1768
+ <ActionMenu />
1769
+ </div>
1770
+ )}
1771
+ {sceneOverlay}
1772
+ </SceneRegion>
1773
+ {/* The inspector and the shortcuts card share one right column. */}
1774
+ <RightStack
1775
+ helper={isCaptureMode ? null : <HelperManager />}
1776
+ inspector={
1777
+ isVersionPreviewMode || isCaptureMode || isStudioMode ? null : (
1778
+ <PanelManager
1779
+ inspectorFooter={inspectorFooter}
1780
+ multiSelectionFooter={multiSelectionFooter}
1781
+ />
1782
+ )
1783
+ }
1784
+ />
1642
1785
  {/* Capture mode drives walk / drone from its own overlay, which
1643
1786
  owns the framing chrome — the walkthrough HUD would both
1644
1787
  clutter the frame and offer a second, conflicting exit. */}
@@ -1655,6 +1798,7 @@ function EditorContent({
1655
1798
  sidebarOverlay={sidebarOverlay}
1656
1799
  sidebarTabs={tabBarTabs}
1657
1800
  stageOverlay={stageOverlay}
1801
+ viewBar={viewBar}
1658
1802
  viewerContent={viewerCanvas}
1659
1803
  viewerToolbarLeft={viewerToolbarLeft}
1660
1804
  viewerToolbarRight={viewerToolbarRight}
@@ -1691,6 +1835,7 @@ function EditorContent({
1691
1835
  isFirstPersonMode={isFirstPersonMode}
1692
1836
  mode={previewStageMode}
1693
1837
  onModeChange={setPreviewStageMode}
1838
+ projectName={projectName}
1694
1839
  showLoader={visibleLoader}
1695
1840
  viewerContent={previewViewerContent}
1696
1841
  />
@@ -1716,12 +1861,7 @@ function EditorContent({
1716
1861
  <div className="pointer-events-auto">
1717
1862
  <ActionMenu />
1718
1863
  </div>
1719
- <div className="pointer-events-auto">
1720
- <PanelManager />
1721
- </div>
1722
- <div className="pointer-events-auto">
1723
- <HelperManager />
1724
- </div>
1864
+ <RightStack helper={<HelperManager />} inspector={<PanelManager />} />
1725
1865
  <RiserDiagramPanel />
1726
1866
  {isFirstPersonMode && (
1727
1867
  <FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />