@pascal-app/editor 1.0.3 → 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 (440) hide show
  1. package/package.json +11 -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 +202 -57
  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 +36 -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 +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -23,6 +23,7 @@ import {
23
23
  ViewerPresentations,
24
24
  } from '@pascal-app/viewer'
25
25
  import {
26
+ type CSSProperties,
26
27
  memo,
27
28
  Profiler,
28
29
  type ProfilerOnRenderCallback,
@@ -38,11 +39,17 @@ import { ViewerZoneSystem } from '../../components/viewer-zone-system'
38
39
  import { type SaveStatus, useAutoSave } from '../../hooks/use-auto-save'
39
40
  import { useKeyboard } from '../../hooks/use-keyboard'
40
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'
41
46
  import {
42
47
  createLocalProjectPresentationPersistence,
43
48
  type LocalProjectPresentationPersistence,
44
49
  } from '../../lib/local-project-presentation-persistence'
45
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'
46
53
  import {
47
54
  applySceneGraphToEditor,
48
55
  loadSceneFromLocalStorage,
@@ -51,16 +58,17 @@ import {
51
58
  } from '../../lib/scene'
52
59
  import { disposeSFXBus, initSFXBus } from '../../lib/sfx-bus'
53
60
  import { useUnitFocusRules } from '../../lib/units'
61
+ import { useViewerFloorDisplay } from '../../lib/viewer-selection'
54
62
  import { type CameraHintAction, useCameraHintFocus } from '../../store/use-camera-hint-focus'
55
63
  import useEditor from '../../store/use-editor'
56
64
  import useFloorplanMode from '../../store/use-floorplan-mode'
57
65
  import useSessionGroups from '../../store/use-session-groups'
66
+ import { paneOfView, VIEW_2D, VIEW_3D, type ViewPaneIndex } from '../../store/view-layout'
58
67
  import { CeilingSelectionAffordanceSystem } from '../systems/ceiling/ceiling-selection-affordance-system'
59
68
  import { CeilingSystem } from '../systems/ceiling/ceiling-system'
60
69
  import { RoofEditSystem } from '../systems/roof/roof-edit-system'
61
70
  import { SelectionAffordanceManager } from '../systems/selection-affordance-manager'
62
71
  import { StairEditSystem } from '../systems/stair/stair-edit-system'
63
- import { ZoneLabelEditorSystem } from '../systems/zone/zone-label-editor-system'
64
72
  import { ZoneSystem } from '../systems/zone/zone-system'
65
73
  import { BoxSelectTool } from '../tools/select/box-select-tool'
66
74
  import { ToolManager } from '../tools/tool-manager'
@@ -73,6 +81,7 @@ import { PanelManager } from '../ui/panels/panel-manager'
73
81
  import { ErrorBoundary } from '../ui/primitives/error-boundary'
74
82
  import { useSidebarStore } from '../ui/primitives/sidebar'
75
83
  import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/primitives/tooltip'
84
+ import { RightStack } from '../ui/right-stack'
76
85
  import { SceneLoader, SceneLoadFailed } from '../ui/scene-loader'
77
86
  import { AppSidebar } from '../ui/sidebar/app-sidebar'
78
87
  import type { ExtraPanel } from '../ui/sidebar/icon-rail'
@@ -80,6 +89,9 @@ import { SettingsPanel, type SettingsPanelProps } from '../ui/sidebar/panels/set
80
89
  import { SitePanel, type SitePanelProps } from '../ui/sidebar/panels/site-panel'
81
90
  import type { SidebarTab } from '../ui/sidebar/tab-bar'
82
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'
83
95
  import { ViewerStage } from '../viewer/viewer-stage'
84
96
  import type { ViewerStageMode } from '../viewer/viewer-stage-modes'
85
97
  import { CaptureCameraRig } from './capture-camera-rig'
@@ -93,21 +105,29 @@ import { FloatingActionMenu } from './floating-action-menu'
93
105
  import { FloatingBuildingActionMenu } from './floating-building-action-menu'
94
106
  import { FloorplanModeCoordinator } from './floorplan-mode-coordinator'
95
107
  import { FloorplanPanel } from './floorplan-panel'
108
+ import { FootprintHeightHandle } from './footprint-height-handle'
96
109
  import { Grid } from './grid'
97
110
  import { GroupFloatingActionMenu } from './group-floating-action-menu'
98
111
  import { GroupRotateHandle } from './group-rotate-handle'
99
112
  import { GroupSelectionBox3D } from './group-selection-box-3d'
113
+ import { EditorHandleHitPriority } from './handles/handle-hit-priority'
100
114
  import { NodeArrowHandles } from './node-arrow-handles'
101
115
  import { QuickMeasurementHud } from './quick-measurement-hud'
102
116
  import { RiserDiagramPanel } from './riser-diagram-panel'
117
+ import { RoomLabels3D } from './room-labels'
103
118
  import { SelectionManager } from './selection-manager'
104
119
  import { SiteEdgeLabels } from './site-edge-labels'
105
120
  import { SlabHoleHighlights } from './slab-hole-highlights'
106
121
  import { SnapshotCaptureOverlay } from './snapshot-capture-overlay'
107
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'
108
127
  import { WallMeasurementLabel } from './wall-measurement-label'
109
128
  import { WallMoveSideHandles } from './wall-move-side-handles'
110
129
  import { WallOpeningHighlights } from './wall-opening-highlights'
130
+ import { WallRegionHandles } from './wall-region-handles'
111
131
 
112
132
  const CAMERA_CONTROLS_HINT_DISMISSED_STORAGE_KEY = 'editor-camera-controls-hint-dismissed:v1'
113
133
  const PREVIEW_STAGE_SWITCHER_POSITION =
@@ -117,12 +137,16 @@ const DELETE_CURSOR_BADGE_OFFSET_X = 14
117
137
  const DELETE_CURSOR_BADGE_OFFSET_Y = 14
118
138
  const PAINT_CURSOR_BADGE_COLOR = '#818cf8'
119
139
  const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
140
+ // Neutral: erasing paints no colour.
141
+ const ERASE_CURSOR_BADGE_COLOR = '#e4e4e7'
120
142
  const PAINT_CURSOR_BADGE_OFFSET_X = 14
121
143
  const PAINT_CURSOR_BADGE_OFFSET_Y = 14
122
144
  const SCENE_READY_FALLBACK_MS = 8000
123
145
  const PRESENTATION_PROJECT_NOT_RESTORED = Symbol('presentation-project-not-restored')
124
146
  const useClientLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect
125
- 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'
126
150
  const recordEditorRender: ProfilerOnRenderCallback = (_id, _phase, actualDuration) => {
127
151
  if (PERF_OVERLAY_ENABLED) recordPerfSample('react-render', actualDuration)
128
152
  }
@@ -130,6 +154,8 @@ const EDITOR_HOVER_STYLES: HoverStyles = {
130
154
  default: { visibleColor: 0x00_aa_ff, hiddenColor: 0xf3_ff_47, strength: 5, pulse: true },
131
155
  delete: { visibleColor: 0xef_44_44, hiddenColor: 0x99_1b_1b, strength: 6, pulse: false },
132
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 },
133
159
  'paint-disabled': {
134
160
  visibleColor: 0x94_a3_b8,
135
161
  hiddenColor: 0x47_55_69,
@@ -177,11 +203,20 @@ export interface EditorProps {
177
203
  viewerToolbarLeft?: ReactNode
178
204
  viewerToolbarRight?: ReactNode
179
205
  /**
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.
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`).
183
210
  */
184
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
185
220
  /**
186
221
  * Docked below the node inspector (v2). Hosts mount the "save as preset"
187
222
  * affordance here so it reads as part of the inspector surface and shows
@@ -243,6 +278,9 @@ export interface EditorProps {
243
278
  sidebarOverlay?: ReactNode
244
279
  viewerBanner?: ReactNode
245
280
 
281
+ /** Shown in the preview header. */
282
+ projectName?: string | null
283
+
246
284
  // Panel config (passed through to sidebar panels — v1 only)
247
285
  settingsPanelProps?: SettingsPanelProps
248
286
  sitePanelProps?: SitePanelProps
@@ -422,7 +460,7 @@ type CameraControlHint = {
422
460
  alternativeKeys?: ShortcutKey[]
423
461
  }
424
462
 
425
- const EDITOR_CAMERA_CONTROL_HINTS: CameraControlHint[] = [
463
+ const CAMERA_CONTROL_HINTS: CameraControlHint[] = [
426
464
  {
427
465
  action: 'Pan',
428
466
  keys: [{ value: 'Space' }, { value: 'Left click' }],
@@ -432,12 +470,6 @@ const EDITOR_CAMERA_CONTROL_HINTS: CameraControlHint[] = [
432
470
  { action: 'Zoom', keys: [{ value: 'Scroll' }] },
433
471
  ]
434
472
 
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
473
  const CAMERA_SHORTCUT_KEY_META: Record<string, { icon?: string; label: string; text?: string }> = {
442
474
  'Left click': {
443
475
  icon: 'ph:mouse-left-click-fill',
@@ -544,14 +576,8 @@ function CameraControlHintItem({ hint }: { hint: CameraControlHint }) {
544
576
  )
545
577
  }
546
578
 
547
- function ViewerCanvasControlsHint({
548
- isPreviewMode,
549
- onDismiss,
550
- }: {
551
- isPreviewMode: boolean
552
- onDismiss: () => void
553
- }) {
554
- const all = isPreviewMode ? PREVIEW_CAMERA_CONTROL_HINTS : EDITOR_CAMERA_CONTROL_HINTS
579
+ function ViewerCanvasControlsHint({ onDismiss }: { onDismiss: () => void }) {
580
+ const all = CAMERA_CONTROL_HINTS
555
581
  // A host teaching one gesture at a time narrows this to the one it is asking
556
582
  // for, and to nothing once it is done. Null — the default — is all of them.
557
583
  const focus = useCameraHintFocus((state) => state.actions)
@@ -680,26 +706,31 @@ function PaintCursorBadge({
680
706
  swatchColor,
681
707
  swatchImageUrl,
682
708
  isEraser,
709
+ isPicker = false,
683
710
  }: {
684
711
  position: { x: number; y: number }
685
712
  state: PaintCursorBadgeState
686
713
  swatchColor: string
687
714
  swatchImageUrl?: string
688
715
  isEraser: boolean
716
+ /** The eyedropper: the swatch is what a click would take. */
717
+ isPicker?: boolean
689
718
  }) {
690
719
  const accentColor =
691
720
  state === 'ready'
692
721
  ? isEraser
693
- ? PAINT_CURSOR_BADGE_COLOR
722
+ ? ERASE_CURSOR_BADGE_COLOR
694
723
  : swatchColor
695
724
  : PAINT_CURSOR_BADGE_DISABLED_COLOR
696
- 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
697
726
  const lineHeight = 18
698
727
 
699
728
  return (
700
729
  <div
701
730
  aria-hidden="true"
702
731
  className="pointer-events-none absolute z-40"
732
+ data-paint-cursor-state={state}
733
+ data-paint-cursor-tool={isEraser ? 'erase' : isPicker ? 'pick' : 'paint'}
703
734
  style={{
704
735
  left: position.x,
705
736
  top: position.y,
@@ -721,29 +752,32 @@ function PaintCursorBadge({
721
752
  transform: `translate(-50%, calc(-100% - ${lineHeight}px))`,
722
753
  }}
723
754
  >
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
- ) : (
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 : (
747
781
  <span
748
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)]"
749
783
  style={{
@@ -751,6 +785,7 @@ function PaintCursorBadge({
751
785
  backgroundImage: swatchImageUrl
752
786
  ? `url(${JSON.stringify(swatchImageUrl)})`
753
787
  : undefined,
788
+ opacity: state === 'aim' ? 0.55 : 1,
754
789
  }}
755
790
  />
756
791
  )
@@ -815,16 +850,20 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
815
850
  <SceneEnvironment />
816
851
  {!(isFirstPersonMode || isStudioMode || isCaptureMode) && <SelectionManager />}
817
852
  {!(noEditing || isXRMode) && <BoxSelectTool />}
853
+ {!noEditing && <EditorHandleHitPriority />}
818
854
  {!noEditing && <NodeArrowHandles />}
855
+ {!noEditing && <FootprintHeightHandle />}
819
856
  {!noEditing && <GroupRotateHandle />}
820
857
  {!noEditing && <GroupSelectionBox3D />}
821
858
  {!noEditing && <WallOpeningHighlights />}
822
859
  {!noEditing && <SlabHoleHighlights />}
823
860
  {!noEditing && <WallMoveSideHandles />}
861
+ {!noEditing && <WallRegionHandles />}
824
862
  {!noEditing && <FenceTangentLines3D />}
825
863
  {!(noEditing || isXRMode) && <FloatingActionMenu />}
826
864
  {!(noEditing || isXRMode) && <GroupFloatingActionMenu />}
827
865
  {!(noEditing || isXRMode) && <FloatingBuildingActionMenu />}
866
+ {!(noEditing || isXRMode) && <RoomLabels3D />}
828
867
  {!(isFirstPersonMode || isXRMode) && <WallMeasurementLabel />}
829
868
  <ExportManager />
830
869
  {isFirstPersonMode ? <ViewerZoneSystem /> : <ZoneSystem />}
@@ -839,8 +878,8 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
839
878
  {isCaptureMode && !isXRMode && <CaptureCameraRig />}
840
879
  {!isXRMode && <CustomCameraControls />}
841
880
  {!isXRMode && <ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />}
881
+ {!isXRMode && <VectorEdgeExtractor />}
842
882
  {!(isFirstPersonMode || isXRMode) && <SiteEdgeLabels />}
843
- <InteractiveSystem />
844
883
  {presentationsReady ? <ViewerPresentations /> : null}
845
884
  {!noEditing && viewerSceneSlot}
846
885
  </>
@@ -933,7 +972,10 @@ function PaintCursorLayer({
933
972
  }) {
934
973
  const mode = useEditor((s) => s.mode)
935
974
  const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
936
- 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'
937
979
  const paintHover = useEditor((s) => s.paintHover)
938
980
  const sceneMaterials = useScene((s) => s.materials)
939
981
  const [position, setPosition] = useState<{ x: number; y: number } | null>(null)
@@ -985,14 +1027,26 @@ function PaintCursorLayer({
985
1027
  }
986
1028
  }, [active, containerRef])
987
1029
 
988
- 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()
989
1034
  const badgeState: PaintCursorBadgeState = !hasPaint
990
1035
  ? 'empty'
991
- : paintHover != null
992
- ? 'ready'
993
- : 'blocked'
994
- const swatchColor = getActivePaintMaterialSwatchColor(activePaintMaterial, sceneMaterials)
995
- 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)
996
1050
 
997
1051
  if (!active || !position) return null
998
1052
 
@@ -1003,6 +1057,7 @@ function PaintCursorLayer({
1003
1057
  >
1004
1058
  <PaintCursorBadge
1005
1059
  isEraser={paintEraser}
1060
+ isPicker={picking}
1006
1061
  position={{ x: 0, y: 0 }}
1007
1062
  state={badgeState}
1008
1063
  swatchColor={swatchColor}
@@ -1012,8 +1067,8 @@ function PaintCursorLayer({
1012
1067
  )
1013
1068
  }
1014
1069
 
1015
- // ── Viewer canvas: memoized, subscribes to viewMode/floorplanPaneRatio internally ──
1016
- // 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.
1017
1072
 
1018
1073
  const ViewerCanvas = memo(function ViewerCanvas({
1019
1074
  isVersionPreviewMode,
@@ -1046,12 +1101,20 @@ const ViewerCanvas = memo(function ViewerCanvas({
1046
1101
  disablePostFx?: boolean
1047
1102
  immersive?: ViewerImmersiveSession
1048
1103
  }) {
1049
- const viewMode = useEditor((s) => s.viewMode)
1050
- const floorplanPaneRatio = useEditor((s) => s.floorplanPaneRatio)
1051
- const setFloorplanPaneRatio = useEditor((s) => s.setFloorplanPaneRatio)
1104
+ const layout = useActiveViewLayout()
1105
+ const views = useEditorViews()
1052
1106
  const isPreviewMode = useEditor((s) => s.isPreviewMode)
1053
1107
  const isCaptureMode = useEditor((s) => s.isCaptureMode)
1054
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)
1055
1118
  const presetIsolation = useEditor((s) =>
1056
1119
  s.captureMode.mode === 'preset' ? s.captureMode.isolated : null,
1057
1120
  )
@@ -1069,25 +1132,24 @@ const ViewerCanvas = memo(function ViewerCanvas({
1069
1132
  setViewerAreaEl(el)
1070
1133
  }, [])
1071
1134
  const viewer3dRef = useRef<HTMLDivElement>(null)
1072
- const isResizingFloorplan = useRef(false)
1135
+ const isResizingPanes = useRef(false)
1073
1136
 
1074
- const handleFloorplanDividerDown = useCallback((e: React.PointerEvent) => {
1137
+ const handlePaneDividerDown = useCallback((e: React.PointerEvent) => {
1075
1138
  e.preventDefault()
1076
- isResizingFloorplan.current = true
1139
+ isResizingPanes.current = true
1077
1140
  document.body.style.cursor = 'col-resize'
1078
1141
  document.body.style.userSelect = 'none'
1079
1142
  }, [])
1080
1143
 
1081
1144
  useEffect(() => {
1082
1145
  const handlePointerMove = (e: PointerEvent) => {
1083
- if (!isResizingFloorplan.current) return
1146
+ if (!isResizingPanes.current) return
1084
1147
  if (!viewerAreaRef.current) return
1085
1148
  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)))
1149
+ useEditor.getState().setViewPaneRatio((e.clientX - rect.left) / rect.width)
1088
1150
  }
1089
1151
  const handlePointerUp = () => {
1090
- isResizingFloorplan.current = false
1152
+ isResizingPanes.current = false
1091
1153
  document.body.style.cursor = ''
1092
1154
  document.body.style.userSelect = ''
1093
1155
  }
@@ -1097,7 +1159,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
1097
1159
  window.removeEventListener('pointermove', handlePointerMove)
1098
1160
  window.removeEventListener('pointerup', handlePointerUp)
1099
1161
  }
1100
- }, [setFloorplanPaneRatio])
1162
+ }, [])
1101
1163
 
1102
1164
  useEffect(() => {
1103
1165
  setIsCameraControlsHintVisible(!readCameraControlsHintDismissed())
@@ -1108,91 +1170,128 @@ const ViewerCanvas = memo(function ViewerCanvas({
1108
1170
  writeCameraControlsHintDismissed(true)
1109
1171
  }, [])
1110
1172
 
1111
- const show2d = viewMode === '2d' || viewMode === 'split'
1112
- 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
+ }
1113
1187
 
1114
1188
  return (
1115
1189
  <ErrorBoundary fallback={<EditorSceneCrashFallback />}>
1116
1190
  {/* `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}>
1191
+ every layout) can anchor to this container's bottom-left. */}
1192
+ <div className="relative h-full" ref={setViewerAreaNode}>
1119
1193
  <QuickMeasurementHud />
1120
1194
  <DeleteConfirmationDialog />
1121
- {/* 2D floorplan — always mounted once shown, hidden via CSS to preserve state */}
1122
1195
  <div
1123
- className="relative h-full flex-shrink-0"
1196
+ className="absolute inset-0 grid"
1197
+ data-view-split={layout.split || undefined}
1124
1198
  style={{
1125
- width: viewMode === '2d' ? '100%' : `${floorplanPaneRatio * 100}%`,
1126
- 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)',
1127
1203
  }}
1128
1204
  >
1129
- <div className="h-full w-full overflow-hidden">
1130
- <FloorplanPanel compassHost={viewerAreaEl} floorplanSceneSlot={floorplanSceneSlot} />
1131
- </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" />
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} />
1138
1214
  </div>
1139
- )}
1140
- </div>
1215
+ </div>
1141
1216
 
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'}
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)}
1181
1225
  >
1182
- <ViewerSceneContent
1183
- isFirstPersonMode={isFirstPersonMode}
1184
- isLoading={showLoader}
1185
- isXRMode={immersive != null}
1186
- isStudioMode={isStudioMode}
1226
+ <DeleteCursorLayer
1227
+ containerRef={viewer3dRef}
1187
1228
  isVersionPreviewMode={isVersionPreviewMode}
1188
- onThumbnailCapture={onThumbnailCapture}
1189
- presentationsReady={presentationsReady}
1190
- viewerSceneSlot={viewerSceneSlot}
1191
1229
  />
1192
- </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
+ })}
1193
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
+ )}
1194
1294
  </div>
1195
- {!(showLoader || isVersionPreviewMode) && <ZoneLabelEditorSystem />}
1196
1295
  </ErrorBoundary>
1197
1296
  )
1198
1297
  })
@@ -1201,12 +1300,14 @@ function PreviewStage({
1201
1300
  isFirstPersonMode,
1202
1301
  mode,
1203
1302
  onModeChange,
1303
+ projectName,
1204
1304
  showLoader,
1205
1305
  viewerContent,
1206
1306
  }: {
1207
1307
  isFirstPersonMode: boolean
1208
1308
  mode: ViewerStageMode
1209
1309
  onModeChange: (mode: ViewerStageMode) => void
1310
+ projectName?: string | null
1210
1311
  showLoader: boolean
1211
1312
  viewerContent: ReactNode
1212
1313
  }) {
@@ -1222,6 +1323,7 @@ function PreviewStage({
1222
1323
  [onModeChange],
1223
1324
  )
1224
1325
 
1326
+ useViewerFloorDisplay()
1225
1327
  const stageMode = isFirstPersonMode || !hasFloorplan ? '3d' : mode
1226
1328
  const stageModes = hasFloorplan && !isFirstPersonMode ? undefined : (['3d'] as const)
1227
1329
 
@@ -1233,9 +1335,11 @@ function PreviewStage({
1233
1335
  <ViewerOverlay
1234
1336
  hideBottomBar={stageMode !== '3d'}
1235
1337
  onBack={() => useEditor.getState().setPreviewMode(false)}
1338
+ projectName={projectName}
1236
1339
  />
1237
1340
  )}
1238
1341
 
1342
+ {stageMode === '3d' && !isFirstPersonMode ? <ViewerHoverLabel /> : null}
1239
1343
  <ViewerStage
1240
1344
  className="absolute inset-0"
1241
1345
  mode={stageMode}
@@ -1261,6 +1365,8 @@ function EditorContent({
1261
1365
  viewerToolbarLeft,
1262
1366
  viewerToolbarRight,
1263
1367
  stageOverlay,
1368
+ sceneOverlay,
1369
+ viewBar = true,
1264
1370
  inspectorFooter,
1265
1371
  multiSelectionFooter,
1266
1372
  viewerSceneSlot,
@@ -1280,6 +1386,7 @@ function EditorContent({
1280
1386
  immersive,
1281
1387
  sidebarOverlay,
1282
1388
  viewerBanner,
1389
+ projectName,
1283
1390
  settingsPanelProps,
1284
1391
  sitePanelProps,
1285
1392
  extraSidebarPanels,
@@ -1311,8 +1418,9 @@ function EditorContent({
1311
1418
  }, [presentationProjectId])
1312
1419
 
1313
1420
  useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode || isStudioMode })
1421
+ useGestureLifecycleOwner()
1314
1422
 
1315
- const { isLoadingSceneRef, saveNow } = useAutoSave({
1423
+ const { beginSceneLoad, completeSceneLoad, saveNow } = useAutoSave({
1316
1424
  guardAgainstSceneWipe,
1317
1425
  onSave,
1318
1426
  onDirty,
@@ -1352,6 +1460,7 @@ function EditorContent({
1352
1460
  const teardown = initializeEditorRuntime()
1353
1461
  return teardown
1354
1462
  }, [])
1463
+ useCeilingEditSessionOwner()
1355
1464
 
1356
1465
  useEffect(() => {
1357
1466
  void useEditor.persist.rehydrate()
@@ -1361,6 +1470,7 @@ function EditorContent({
1361
1470
  useEffect(() => {
1362
1471
  useViewer.getState().setProjectId(projectId ?? null)
1363
1472
  useFloorplanMode.getState().setProjectId(projectId ?? null)
1473
+ resetPaintMode()
1364
1474
 
1365
1475
  return () => {
1366
1476
  useViewer.getState().setProjectId(null)
@@ -1373,7 +1483,7 @@ function EditorContent({
1373
1483
  let cancelled = false
1374
1484
 
1375
1485
  async function load(attempt: number) {
1376
- isLoadingSceneRef.current = true
1486
+ beginSceneLoad()
1377
1487
  setSceneLoadError(null)
1378
1488
  setHasLoadedInitialScene(false)
1379
1489
  setIsViewerSceneReady(false)
@@ -1388,6 +1498,9 @@ function EditorContent({
1388
1498
  const sceneGraph = onLoad ? await onLoad() : loadSceneFromLocalStorage()
1389
1499
  if (!cancelled && attempt === sceneLoadAttempt) {
1390
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()
1391
1504
  setIsViewerSceneReady(false)
1392
1505
  setSceneReadyKey((key) => key + 1)
1393
1506
  }
@@ -1404,9 +1517,11 @@ function EditorContent({
1404
1517
  setIsSceneLoading(false)
1405
1518
  if (!failed) {
1406
1519
  setHasLoadedInitialScene(true)
1407
- requestAnimationFrame(() => {
1408
- isLoadingSceneRef.current = false
1409
- })
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' })
1410
1525
  }
1411
1526
  }
1412
1527
  }
@@ -1417,7 +1532,7 @@ function EditorContent({
1417
1532
  return () => {
1418
1533
  cancelled = true
1419
1534
  }
1420
- }, [onLoad, isLoadingSceneRef, sceneLoadAttempt])
1535
+ }, [onLoad, beginSceneLoad, completeSceneLoad, sceneLoadAttempt])
1421
1536
 
1422
1537
  const retrySceneLoad = useCallback(() => {
1423
1538
  setSceneLoadAttempt((attempt) => attempt + 1)
@@ -1486,10 +1601,13 @@ function EditorContent({
1486
1601
 
1487
1602
  const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
1488
1603
  const wasFirstPersonModeRef = useRef(isFirstPersonMode)
1604
+ const firstPersonWasPreviewRef = useRef(isPreviewMode)
1489
1605
 
1490
1606
  useEffect(() => {
1491
1607
  const wasFirstPersonMode = wasFirstPersonModeRef.current
1608
+ const wasPreviewMode = firstPersonWasPreviewRef.current
1492
1609
  wasFirstPersonModeRef.current = isFirstPersonMode
1610
+ firstPersonWasPreviewRef.current = isPreviewMode
1493
1611
 
1494
1612
  if (isFirstPersonMode && !wasFirstPersonMode) {
1495
1613
  const viewer = useViewer.getState()
@@ -1508,7 +1626,8 @@ function EditorContent({
1508
1626
  firstPersonPreviousLevelRef.current = null
1509
1627
  viewer.setWalkthroughMode(false)
1510
1628
 
1511
- if (!previousLevelId) return
1629
+ // Leaving Preview has already restored the editor's complete selection.
1630
+ if ((wasPreviewMode && !isPreviewMode) || !previousLevelId) return
1512
1631
 
1513
1632
  const previousLevelNode = useScene.getState().nodes[previousLevelId]
1514
1633
  if (previousLevelNode?.type === 'level') {
@@ -1518,7 +1637,7 @@ function EditorContent({
1518
1637
  selectedIds: [],
1519
1638
  })
1520
1639
  }
1521
- }, [isFirstPersonMode])
1640
+ }, [isFirstPersonMode, isPreviewMode])
1522
1641
 
1523
1642
  const previewViewerContent = (
1524
1643
  <Viewer
@@ -1526,16 +1645,18 @@ function EditorContent({
1526
1645
  disablePostFx={disablePostFx}
1527
1646
  hoverStyles={EDITOR_HOVER_STYLES}
1528
1647
  renderContext="editor"
1529
- selectionManager="default"
1648
+ selectionManager="custom"
1530
1649
  >
1531
1650
  <ExportManager />
1532
- <ViewerZoneSystem />
1651
+ <ViewerSelectionManager />
1652
+ <ParametricViewerRooms />
1533
1653
  <CeilingSystem />
1534
1654
  <RoofEditSystem />
1535
1655
  <StairEditSystem />
1536
1656
  {isFirstPersonMode && <FirstPersonControls />}
1537
1657
  <CustomCameraControls />
1538
1658
  <ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
1659
+ <VectorEdgeExtractor />
1539
1660
  <InteractiveSystem />
1540
1661
  {presentationsReady ? <ViewerPresentations /> : null}
1541
1662
  </Viewer>
@@ -1605,6 +1726,10 @@ function EditorContent({
1605
1726
  mobileDefaultSnap: 0.5,
1606
1727
  mobileIcon: p.icon,
1607
1728
  icon: p.icon,
1729
+ noPanel: p.noPanel,
1730
+ onSelect: p.onSelect,
1731
+ stageActive: p.stageActive,
1732
+ onDeselect: p.onDeselect,
1608
1733
  })),
1609
1734
  ]
1610
1735
 
@@ -1626,6 +1751,7 @@ function EditorContent({
1626
1751
  isFirstPersonMode={isFirstPersonMode}
1627
1752
  mode={previewStageMode}
1628
1753
  onModeChange={setPreviewStageMode}
1754
+ projectName={projectName}
1629
1755
  showLoader={visibleLoader}
1630
1756
  viewerContent={previewViewerContent}
1631
1757
  />
@@ -1635,25 +1761,27 @@ function EditorContent({
1635
1761
  navbarSlot={navbarSlot}
1636
1762
  overlays={
1637
1763
  <>
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
- )}
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
+ />
1657
1785
  {/* Capture mode drives walk / drone from its own overlay, which
1658
1786
  owns the framing chrome — the walkthrough HUD would both
1659
1787
  clutter the frame and offer a second, conflicting exit. */}
@@ -1670,6 +1798,7 @@ function EditorContent({
1670
1798
  sidebarOverlay={sidebarOverlay}
1671
1799
  sidebarTabs={tabBarTabs}
1672
1800
  stageOverlay={stageOverlay}
1801
+ viewBar={viewBar}
1673
1802
  viewerContent={viewerCanvas}
1674
1803
  viewerToolbarLeft={viewerToolbarLeft}
1675
1804
  viewerToolbarRight={viewerToolbarRight}
@@ -1706,6 +1835,7 @@ function EditorContent({
1706
1835
  isFirstPersonMode={isFirstPersonMode}
1707
1836
  mode={previewStageMode}
1708
1837
  onModeChange={setPreviewStageMode}
1838
+ projectName={projectName}
1709
1839
  showLoader={visibleLoader}
1710
1840
  viewerContent={previewViewerContent}
1711
1841
  />
@@ -1731,12 +1861,7 @@ function EditorContent({
1731
1861
  <div className="pointer-events-auto">
1732
1862
  <ActionMenu />
1733
1863
  </div>
1734
- <div className="pointer-events-auto">
1735
- <PanelManager />
1736
- </div>
1737
- <div className="pointer-events-auto">
1738
- <HelperManager />
1739
- </div>
1864
+ <RightStack helper={<HelperManager />} inspector={<PanelManager />} />
1740
1865
  <RiserDiagramPanel />
1741
1866
  {isFirstPersonMode && (
1742
1867
  <FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />