@pascal-app/editor 0.8.0 → 0.9.2

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 (350) hide show
  1. package/package.json +15 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +795 -28
  5. package/src/components/editor/editor-layout-mobile.tsx +8 -3
  6. package/src/components/editor/editor-layout-v2.tsx +76 -51
  7. package/src/components/editor/export-manager.tsx +69 -32
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
  10. package/src/components/editor/first-person/build-collider-world.ts +228 -83
  11. package/src/components/editor/first-person-controls.tsx +943 -27
  12. package/src/components/editor/floating-action-menu.tsx +676 -208
  13. package/src/components/editor/floating-building-action-menu.tsx +2 -2
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +6916 -12137
  17. package/src/components/editor/grid.tsx +197 -36
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +420 -0
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +396 -0
  24. package/src/components/editor/group-transform-shared.ts +489 -0
  25. package/src/components/editor/handles/handle-arrow.tsx +581 -0
  26. package/src/components/editor/handles/use-handle-drag.ts +246 -0
  27. package/src/components/editor/index.tsx +412 -162
  28. package/src/components/editor/measurement-pill.tsx +115 -0
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +1646 -0
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +1106 -483
  36. package/src/components/editor/site-edge-labels.tsx +45 -21
  37. package/src/components/editor/slab-hole-highlights.tsx +474 -0
  38. package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +140 -41
  41. package/src/components/editor/use-floorplan-background-placement.ts +190 -35
  42. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  43. package/src/components/editor/use-floorplan-scene-data.ts +19 -7
  44. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  45. package/src/components/editor/wall-measurement-label.tsx +14 -19
  46. package/src/components/editor/wall-move-side-handles.tsx +870 -0
  47. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  48. package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
  49. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
  50. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  51. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  52. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  53. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  54. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  55. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  56. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  57. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
  58. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
  59. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  60. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
  61. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  62. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  63. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  64. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  65. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  66. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
  67. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  68. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
  69. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  70. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  71. package/src/components/editor-2d/svg-paths.ts +10 -2
  72. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
  73. package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
  74. package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
  75. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  76. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  77. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  78. package/src/components/systems/zone/zone-system.tsx +42 -13
  79. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  80. package/src/components/tools/elevator/elevator-tool.tsx +375 -0
  81. package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
  82. package/src/components/tools/fence/fence-drafting.ts +141 -27
  83. package/src/components/tools/item/move-tool.tsx +45 -105
  84. package/src/components/tools/item/placement-math.test.ts +22 -0
  85. package/src/components/tools/item/placement-math.ts +59 -7
  86. package/src/components/tools/item/placement-strategies.ts +432 -16
  87. package/src/components/tools/item/placement-types.ts +22 -1
  88. package/src/components/tools/item/use-draft-node.ts +51 -2
  89. package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
  90. package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
  91. package/src/components/tools/roof/roof-tool.tsx +350 -28
  92. package/src/components/tools/select/box-select-state.ts +88 -0
  93. package/src/components/tools/select/box-select-tool.tsx +442 -520
  94. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  95. package/src/components/tools/select/marquee-geometry.ts +155 -0
  96. package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
  97. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  98. package/src/components/tools/select/select-candidates.test.ts +109 -0
  99. package/src/components/tools/select/select-candidates.ts +85 -0
  100. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  101. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  102. package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
  103. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  104. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  105. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  106. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  107. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  108. package/src/components/tools/shared/placement-box.tsx +305 -0
  109. package/src/components/tools/shared/polygon-editor.tsx +866 -175
  110. package/src/components/tools/shared/segment-angle.ts +56 -0
  111. package/src/components/tools/site/site-boundary-editor.tsx +520 -16
  112. package/src/components/tools/site/site-flag-model.ts +18 -0
  113. package/src/components/tools/stair/stair-defaults.ts +2 -1
  114. package/src/components/tools/stair/stair-tool.tsx +342 -37
  115. package/src/components/tools/tool-manager.tsx +239 -62
  116. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  117. package/src/components/tools/wall/wall-drafting.ts +248 -171
  118. package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
  119. package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
  120. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  121. package/src/components/tools/zone/zone-tool.tsx +86 -82
  122. package/src/components/ui/action-menu/action-button.tsx +21 -1
  123. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  124. package/src/components/ui/action-menu/control-modes.tsx +40 -170
  125. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  126. package/src/components/ui/action-menu/index.tsx +3 -117
  127. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  128. package/src/components/ui/action-menu/structure-tools.tsx +27 -93
  129. package/src/components/ui/action-menu/view-toggles.tsx +279 -90
  130. package/src/components/ui/command-palette/editor-commands.tsx +20 -15
  131. package/src/components/ui/command-palette/index.tsx +5 -3
  132. package/src/components/ui/controls/material-paint-panel.tsx +152 -0
  133. package/src/components/ui/controls/material-picker.tsx +87 -177
  134. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  135. package/src/components/ui/controls/metric-control.tsx +142 -46
  136. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  137. package/src/components/ui/controls/slider-control.tsx +88 -38
  138. package/src/components/ui/floating-level-selector.tsx +47 -10
  139. package/src/components/ui/helpers/building-helper.tsx +10 -23
  140. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  141. package/src/components/ui/helpers/helper-manager.tsx +235 -23
  142. package/src/components/ui/helpers/item-helper.tsx +28 -32
  143. package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
  144. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  145. package/src/components/ui/icon-ref.tsx +47 -0
  146. package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
  147. package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
  148. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  149. package/src/components/ui/panels/node-display.ts +17 -16
  150. package/src/components/ui/panels/panel-manager.tsx +55 -58
  151. package/src/components/ui/panels/panel-wrapper.tsx +238 -12
  152. package/src/components/ui/panels/parametric-inspector.tsx +482 -0
  153. package/src/components/ui/panels/reference-panel.tsx +54 -19
  154. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  155. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  156. package/src/components/ui/primitives/sidebar.tsx +1 -0
  157. package/src/components/ui/scene-loader.tsx +1 -3
  158. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  159. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  160. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  161. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  162. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  163. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
  164. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
  165. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
  166. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  167. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
  168. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
  169. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  170. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  171. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
  172. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  173. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
  174. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
  175. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
  176. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  177. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  178. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
  179. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
  180. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
  181. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
  182. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
  183. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  184. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  185. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
  186. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  187. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  188. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  189. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  190. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  191. package/src/components/ui/sidebar/tab-bar.tsx +75 -1
  192. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  193. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  194. package/src/components/ui/snap-target-badge.tsx +88 -0
  195. package/src/components/viewer-overlay.tsx +253 -61
  196. package/src/components/viewer-zone-system.tsx +7 -2
  197. package/src/hooks/use-auto-save.test.ts +14 -0
  198. package/src/hooks/use-auto-save.ts +64 -10
  199. package/src/hooks/use-ceiling-events.ts +176 -0
  200. package/src/hooks/use-drag-action.ts +127 -0
  201. package/src/hooks/use-keyboard.ts +493 -95
  202. package/src/hooks/use-selection.ts +64 -0
  203. package/src/index.tsx +451 -6
  204. package/src/lib/active-placement-surface.ts +35 -0
  205. package/src/lib/ceiling-plan-snap.ts +24 -0
  206. package/src/lib/constants.ts +6 -2
  207. package/src/lib/contextual-help.test.ts +112 -0
  208. package/src/lib/contextual-help.ts +144 -0
  209. package/src/lib/continuation.ts +64 -0
  210. package/src/lib/direct-manipulation.test.ts +192 -0
  211. package/src/lib/direct-manipulation.ts +109 -0
  212. package/src/lib/editor-api.ts +50 -0
  213. package/src/lib/elevator-support.ts +96 -0
  214. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  215. package/src/lib/floorplan/apply-alignment.ts +117 -0
  216. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  217. package/src/lib/floorplan/geometry.ts +53 -0
  218. package/src/lib/floorplan/index.ts +10 -0
  219. package/src/lib/floorplan/items.ts +5 -2
  220. package/src/lib/floorplan/plan-coords.ts +21 -0
  221. package/src/lib/floorplan/selection-tool.ts +40 -0
  222. package/src/lib/floorplan/stairs.ts +12 -1
  223. package/src/lib/fresh-planar-placement.test.ts +339 -0
  224. package/src/lib/fresh-planar-placement.ts +130 -0
  225. package/src/lib/glb-export.test.ts +441 -0
  226. package/src/lib/glb-export.ts +874 -0
  227. package/src/lib/history.ts +11 -1
  228. package/src/lib/interaction/hot-set.test.ts +133 -0
  229. package/src/lib/interaction/hot-set.ts +67 -0
  230. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  231. package/src/lib/interaction/overlay-policy.ts +59 -0
  232. package/src/lib/interaction/scope.ts +211 -0
  233. package/src/lib/level-duplication.ts +54 -42
  234. package/src/lib/level-selection.ts +2 -2
  235. package/src/lib/material-paint.ts +227 -48
  236. package/src/lib/measurement-kind.test.ts +30 -0
  237. package/src/lib/measurement-kind.ts +19 -0
  238. package/src/lib/measurement-label.test.ts +47 -0
  239. package/src/lib/measurement-label.ts +68 -0
  240. package/src/lib/measurement-parser.ts +116 -0
  241. package/src/lib/measurements.test.ts +183 -0
  242. package/src/lib/measurements.ts +199 -0
  243. package/src/lib/paint-scope.test.ts +454 -0
  244. package/src/lib/paint-scope.ts +461 -0
  245. package/src/lib/placement-drag-release.ts +23 -0
  246. package/src/lib/placement-metadata.ts +29 -0
  247. package/src/lib/planar-cursor-placement.test.ts +100 -0
  248. package/src/lib/planar-cursor-placement.ts +42 -0
  249. package/src/lib/plugin-panels.test.ts +19 -0
  250. package/src/lib/plugin-panels.ts +91 -0
  251. package/src/lib/quick-measurement.test.ts +77 -0
  252. package/src/lib/quick-measurement.ts +109 -0
  253. package/src/lib/roof-duplication.ts +7 -7
  254. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  255. package/src/lib/roof-wall-hit.ts +164 -0
  256. package/src/lib/scene-clipboard.test.ts +196 -0
  257. package/src/lib/scene-clipboard.ts +332 -0
  258. package/src/lib/scene.ts +40 -8
  259. package/src/lib/selection-routing.test.ts +298 -0
  260. package/src/lib/selection-routing.ts +175 -0
  261. package/src/lib/sfx/index.ts +1 -0
  262. package/src/lib/sfx/movement-tick.test.ts +65 -0
  263. package/src/lib/sfx/movement-tick.ts +18 -0
  264. package/src/lib/sfx-bus.ts +69 -10
  265. package/src/lib/sfx-player.test.ts +124 -0
  266. package/src/lib/sfx-player.ts +148 -55
  267. package/src/lib/site-boundary.ts +1 -0
  268. package/src/lib/slab-plan-snap.test.ts +93 -0
  269. package/src/lib/slab-plan-snap.ts +108 -0
  270. package/src/lib/snapping-mode.test.ts +138 -0
  271. package/src/lib/snapping-mode.ts +179 -0
  272. package/src/lib/stair-duplication.ts +4 -4
  273. package/src/lib/stair-levels.test.ts +146 -0
  274. package/src/lib/stair-levels.ts +177 -0
  275. package/src/lib/surface-plan-snap.test.ts +71 -0
  276. package/src/lib/surface-plan-snap.ts +256 -0
  277. package/src/lib/use-linear-display.ts +40 -0
  278. package/src/lib/world-grid-snap.ts +231 -0
  279. package/src/lib/zone-content.ts +130 -0
  280. package/src/store/use-alignment-guides.ts +21 -0
  281. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  282. package/src/store/use-editor.tsx +767 -114
  283. package/src/store/use-facing-pose.ts +44 -0
  284. package/src/store/use-fence-curve-draft.ts +21 -0
  285. package/src/store/use-floorplan-draft-preview.ts +101 -0
  286. package/src/store/use-floorplan-marquee.ts +50 -0
  287. package/src/store/use-interaction-scope.test.ts +186 -0
  288. package/src/store/use-interaction-scope.ts +132 -0
  289. package/src/store/use-measurement-draft.test.ts +530 -0
  290. package/src/store/use-measurement-draft.ts +543 -0
  291. package/src/store/use-opening-guides.ts +41 -0
  292. package/src/store/use-placement-preview.ts +39 -0
  293. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  294. package/src/store/use-quick-measurement-hud.ts +77 -0
  295. package/src/store/use-segment-draft-chain.ts +28 -0
  296. package/src/store/use-stair-build-preview.ts +43 -0
  297. package/src/store/use-wall-move-ghosts.ts +36 -0
  298. package/src/store/use-wall-snap-indicator.ts +34 -0
  299. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  300. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  301. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  302. package/src/components/tools/building/move-building-tool.tsx +0 -157
  303. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  304. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  305. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  306. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  307. package/src/components/tools/column/column-tool.tsx +0 -97
  308. package/src/components/tools/column/move-column-tool.tsx +0 -105
  309. package/src/components/tools/door/door-math.ts +0 -110
  310. package/src/components/tools/door/door-tool.tsx +0 -324
  311. package/src/components/tools/door/move-door-tool.tsx +0 -412
  312. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  313. package/src/components/tools/fence/fence-tool.tsx +0 -346
  314. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  315. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  316. package/src/components/tools/item/item-tool.tsx +0 -26
  317. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  318. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  319. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  320. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  321. package/src/components/tools/slab/slab-tool.tsx +0 -322
  322. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  323. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  324. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  325. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  326. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  327. package/src/components/tools/wall/wall-tool.tsx +0 -332
  328. package/src/components/tools/window/move-window-tool.tsx +0 -447
  329. package/src/components/tools/window/window-math.ts +0 -117
  330. package/src/components/tools/window/window-tool.tsx +0 -332
  331. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  332. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  333. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  334. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  335. package/src/components/ui/panels/column-panel.tsx +0 -759
  336. package/src/components/ui/panels/door-panel.tsx +0 -1299
  337. package/src/components/ui/panels/fence-panel.tsx +0 -222
  338. package/src/components/ui/panels/item-panel.tsx +0 -306
  339. package/src/components/ui/panels/paint-panel.tsx +0 -163
  340. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  341. package/src/components/ui/panels/roof-panel.tsx +0 -280
  342. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  343. package/src/components/ui/panels/slab-panel.tsx +0 -250
  344. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  345. package/src/components/ui/panels/stair-panel.tsx +0 -577
  346. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  347. package/src/components/ui/panels/wall-panel.tsx +0 -273
  348. package/src/components/ui/panels/window-panel.tsx +0 -968
  349. package/src/contexts/presets-context.tsx +0 -121
  350. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -2,38 +2,39 @@
2
2
 
3
3
  import { Icon } from '@iconify/react'
4
4
  import {
5
+ getCatalogMaterialById,
6
+ getLibraryMaterialIdFromRef,
7
+ getSceneMaterialIdFromRef,
5
8
  initSpaceDetectionSync,
6
9
  initSpatialGridSync,
7
10
  spatialGridManager,
8
11
  useScene,
9
12
  } from '@pascal-app/core'
10
- import { type HoverStyles, InteractiveSystem, useViewer, Viewer } from '@pascal-app/viewer'
11
13
  import {
12
- memo,
13
- type ReactNode,
14
- useCallback,
15
- useEffect,
16
- useLayoutEffect,
17
- useRef,
18
- useState,
19
- } from 'react'
14
+ type HoverStyles,
15
+ InteractiveSystem,
16
+ SceneEnvironment,
17
+ useViewer,
18
+ Viewer,
19
+ } from '@pascal-app/viewer'
20
+ import { memo, type ReactNode, useCallback, useEffect, useRef, useState } from 'react'
20
21
  import { ViewerOverlay } from '../../components/viewer-overlay'
21
22
  import { ViewerZoneSystem } from '../../components/viewer-zone-system'
22
- import { type PresetsAdapter, PresetsProvider } from '../../contexts/presets-context'
23
- import { useAutoFrame } from '../../hooks/use-auto-frame'
24
23
  import { type SaveStatus, useAutoSave } from '../../hooks/use-auto-save'
25
24
  import { useKeyboard } from '../../hooks/use-keyboard'
25
+ import { type ActivePaintMaterial, hasActivePaintMaterial } from '../../lib/material-paint'
26
26
  import {
27
27
  applySceneGraphToEditor,
28
28
  loadSceneFromLocalStorage,
29
29
  type SceneGraph,
30
30
  writePersistedSelection,
31
31
  } from '../../lib/scene'
32
- import { initSFXBus } from '../../lib/sfx-bus'
32
+ import { disposeSFXBus, initSFXBus } from '../../lib/sfx-bus'
33
33
  import useEditor from '../../store/use-editor'
34
34
  import { CeilingSelectionAffordanceSystem } from '../systems/ceiling/ceiling-selection-affordance-system'
35
35
  import { CeilingSystem } from '../systems/ceiling/ceiling-system'
36
36
  import { RoofEditSystem } from '../systems/roof/roof-edit-system'
37
+ import { SelectionAffordanceManager } from '../systems/selection-affordance-manager'
37
38
  import { StairEditSystem } from '../systems/stair/stair-edit-system'
38
39
  import { ZoneLabelEditorSystem } from '../systems/zone/zone-label-editor-system'
39
40
  import { ZoneSystem } from '../systems/zone/zone-system'
@@ -54,29 +55,41 @@ import type { ExtraPanel } from '../ui/sidebar/icon-rail'
54
55
  import { SettingsPanel, type SettingsPanelProps } from '../ui/sidebar/panels/settings-panel'
55
56
  import { SitePanel, type SitePanelProps } from '../ui/sidebar/panels/site-panel'
56
57
  import type { SidebarTab } from '../ui/sidebar/tab-bar'
58
+ import { useHostPanels } from '../ui/sidebar/use-plugin-panels'
57
59
  import { CustomCameraControls } from './custom-camera-controls'
58
60
  import { EditorLayoutV2 } from './editor-layout-v2'
59
61
  import { ExportManager } from './export-manager'
62
+ import { FenceTangentLines3D } from './fence-tangent-lines-3d'
60
63
  import { FirstPersonControls, FirstPersonOverlay } from './first-person-controls'
61
64
  import { FloatingActionMenu } from './floating-action-menu'
62
65
  import { FloatingBuildingActionMenu } from './floating-building-action-menu'
63
66
  import { FloorplanPanel } from './floorplan-panel'
64
67
  import { Grid } from './grid'
65
- import { PresetThumbnailGenerator } from './preset-thumbnail-generator'
68
+ import { GroupFloatingActionMenu } from './group-floating-action-menu'
69
+ import { GroupRotateHandle } from './group-rotate-handle'
70
+ import { GroupSelectionBox3D } from './group-selection-box-3d'
71
+ import { NodeArrowHandles } from './node-arrow-handles'
72
+ import { QuickMeasurementHud } from './quick-measurement-hud'
73
+ import { RiserDiagramPanel } from './riser-diagram-panel'
66
74
  import { SelectionManager } from './selection-manager'
67
75
  import { SiteEdgeLabels } from './site-edge-labels'
76
+ import { SlabHoleHighlights } from './slab-hole-highlights'
68
77
  import { SnapshotCaptureOverlay } from './snapshot-capture-overlay'
69
78
  import { type SnapshotCameraData, ThumbnailGenerator } from './thumbnail-generator'
70
79
  import { WallMeasurementLabel } from './wall-measurement-label'
80
+ import { WallMoveSideHandles } from './wall-move-side-handles'
81
+ import { WallOpeningHighlights } from './wall-opening-highlights'
71
82
 
72
83
  const CAMERA_CONTROLS_HINT_DISMISSED_STORAGE_KEY = 'editor-camera-controls-hint-dismissed:v1'
73
84
  const DELETE_CURSOR_BADGE_COLOR = '#ef4444'
74
85
  const DELETE_CURSOR_BADGE_OFFSET_X = 14
75
86
  const DELETE_CURSOR_BADGE_OFFSET_Y = 14
76
- const PAINT_CURSOR_BADGE_COLOR = '#f59e0b'
87
+ const PAINT_CURSOR_BADGE_COLOR = '#818cf8'
77
88
  const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
78
89
  const PAINT_CURSOR_BADGE_OFFSET_X = 14
79
90
  const PAINT_CURSOR_BADGE_OFFSET_Y = 14
91
+ const SCENE_READY_FALLBACK_MS = 8000
92
+ type PaintCursorBadgeState = 'empty' | 'ready' | 'blocked'
80
93
  const EDITOR_HOVER_STYLES: HoverStyles = {
81
94
  default: { visibleColor: 0x00_aa_ff, hiddenColor: 0xf3_ff_47, strength: 5, pulse: true },
82
95
  delete: { visibleColor: 0xef_44_44, hiddenColor: 0x99_1b_1b, strength: 6, pulse: false },
@@ -88,6 +101,7 @@ const EDITOR_HOVER_STYLES: HoverStyles = {
88
101
  pulse: false,
89
102
  },
90
103
  }
104
+ const EDITOR_DEFAULT_RENDER = { shading: 'solid' } as const
91
105
 
92
106
  /**
93
107
  * Wire up module-level singletons (spatial grid, space detection, SFX) for
@@ -106,6 +120,7 @@ function initializeEditorRuntime(): () => void {
106
120
  unsubscribeSpaceDetection?.()
107
121
 
108
122
  spatialGridManager.clear()
123
+ disposeSFXBus()
109
124
 
110
125
  const outliner = useViewer.getState().outliner
111
126
  outliner.selectedObjects.length = 0
@@ -125,12 +140,24 @@ export interface EditorProps {
125
140
  sidebarTabs?: (SidebarTab & { component: React.ComponentType })[]
126
141
  viewerToolbarLeft?: ReactNode
127
142
  viewerToolbarRight?: ReactNode
143
+ /**
144
+ * Full-bleed surface swapped in over the 3D canvas (v2) — e.g. the studio
145
+ * gallery. The canvas stays mounted underneath (no WebGL re-init) and the
146
+ * viewer toolbar stays on top so the host's stage switch remains reachable.
147
+ */
148
+ stageOverlay?: ReactNode
149
+ /**
150
+ * Docked below the node inspector (v2). Hosts mount the "save as preset"
151
+ * affordance here so it reads as part of the inspector surface and shows
152
+ * only while a node is selected.
153
+ */
154
+ inspectorFooter?: ReactNode
128
155
 
129
156
  projectId?: string | null
130
157
 
131
158
  // Persistence — defaults to localStorage when omitted
132
159
  onLoad?: () => Promise<SceneGraph | null>
133
- onSave?: (scene: SceneGraph) => Promise<void>
160
+ onSave?: (scene: SceneGraph, options?: { keepalive?: boolean }) => Promise<void>
134
161
  onDirty?: () => void
135
162
  onSaveStatusChange?: (status: SaveStatus) => void
136
163
 
@@ -141,6 +168,10 @@ export interface EditorProps {
141
168
  // Loading indicator (e.g. project fetching in community mode)
142
169
  isLoading?: boolean
143
170
 
171
+ // Fires when the full-screen scene loader shows/hides — lets hosts measure
172
+ // open-to-interactive time without reaching into internal loader state.
173
+ onLoaderChange?: (visible: boolean) => void
174
+
144
175
  // Thumbnail
145
176
  onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
146
177
 
@@ -153,9 +184,6 @@ export interface EditorProps {
153
184
  sitePanelProps?: SitePanelProps
154
185
  extraSidebarPanels?: ExtraPanel[]
155
186
 
156
- // Presets storage backend (defaults to localStorage)
157
- presetsAdapter?: PresetsAdapter
158
-
159
187
  // Command palette fallback when no commands match
160
188
  commandPaletteEmptyAction?: CommandPaletteEmptyAction
161
189
  }
@@ -334,6 +362,7 @@ const EDITOR_CAMERA_CONTROL_HINTS: CameraControlHint[] = [
334
362
  {
335
363
  action: 'Pan',
336
364
  keys: [{ value: 'Space' }, { value: 'Left click' }],
365
+ alternativeKeys: [{ value: 'Middle click' }],
337
366
  },
338
367
  { action: 'Rotate', keys: [{ value: 'Right click' }] },
339
368
  { action: 'Zoom', keys: [{ value: 'Scroll' }] },
@@ -526,86 +555,210 @@ function DeleteCursorBadge({ position }: { position: { x: number; y: number } })
526
555
  )
527
556
  }
528
557
 
558
+ function getActivePaintMaterialSwatchColor(
559
+ material: ActivePaintMaterial | null,
560
+ sceneMaterials: ReturnType<typeof useScene.getState>['materials'],
561
+ ) {
562
+ const directColor = material?.material?.properties?.color
563
+ if (directColor) return directColor
564
+
565
+ const sceneMaterialId = getSceneMaterialIdFromRef(material?.materialPreset)
566
+ if (sceneMaterialId) {
567
+ const sceneMaterial = sceneMaterials[sceneMaterialId as keyof typeof sceneMaterials]
568
+ const sceneColor = sceneMaterial?.material.properties?.color
569
+ if (sceneColor) return sceneColor
570
+ }
571
+
572
+ const catalogId =
573
+ getLibraryMaterialIdFromRef(material?.materialPreset) ?? material?.material?.id ?? undefined
574
+ const catalogMaterial = getCatalogMaterialById(catalogId)
575
+ return (
576
+ catalogMaterial?.previewColor ??
577
+ catalogMaterial?.preset.mapProperties.color ??
578
+ PAINT_CURSOR_BADGE_COLOR
579
+ )
580
+ }
581
+
582
+ function getActivePaintMaterialSwatchImageUrl(
583
+ material: ActivePaintMaterial | null,
584
+ sceneMaterials: ReturnType<typeof useScene.getState>['materials'],
585
+ ) {
586
+ const directTextureUrl = material?.material?.texture?.url
587
+ if (directTextureUrl) return directTextureUrl
588
+
589
+ const sceneMaterialId = getSceneMaterialIdFromRef(material?.materialPreset)
590
+ if (sceneMaterialId) {
591
+ const sceneMaterial = sceneMaterials[sceneMaterialId as keyof typeof sceneMaterials]
592
+ const sceneTextureUrl = sceneMaterial?.material.texture?.url
593
+ if (sceneTextureUrl) return sceneTextureUrl
594
+ }
595
+
596
+ const catalogId =
597
+ getLibraryMaterialIdFromRef(material?.materialPreset) ?? material?.material?.id ?? undefined
598
+ const catalogMaterial = getCatalogMaterialById(catalogId)
599
+ return catalogMaterial?.previewThumbnailUrl ?? catalogMaterial?.preset.maps.albedoMap
600
+ }
601
+
529
602
  function PaintCursorBadge({
530
603
  position,
531
- label,
532
- disabled,
533
- icon,
604
+ state,
605
+ swatchColor,
606
+ swatchImageUrl,
607
+ isEraser,
534
608
  }: {
535
609
  position: { x: number; y: number }
536
- label: string
537
- disabled: boolean
538
- icon: string
610
+ state: PaintCursorBadgeState
611
+ swatchColor: string
612
+ swatchImageUrl?: string
613
+ isEraser: boolean
539
614
  }) {
540
- const accentColor = disabled ? PAINT_CURSOR_BADGE_DISABLED_COLOR : PAINT_CURSOR_BADGE_COLOR
615
+ const accentColor =
616
+ state === 'ready'
617
+ ? isEraser
618
+ ? PAINT_CURSOR_BADGE_COLOR
619
+ : swatchColor
620
+ : PAINT_CURSOR_BADGE_DISABLED_COLOR
621
+ const iconOpacity = state === 'ready' ? 1 : state === 'blocked' ? 0.62 : 0.42
622
+ const lineHeight = 18
541
623
 
542
624
  return (
543
625
  <div
544
626
  aria-hidden="true"
545
627
  className="pointer-events-none absolute z-40"
546
628
  style={{
547
- left: position.x + PAINT_CURSOR_BADGE_OFFSET_X,
548
- top: position.y + PAINT_CURSOR_BADGE_OFFSET_Y,
629
+ left: position.x,
630
+ top: position.y,
549
631
  }}
550
632
  >
551
633
  <div
552
- className="flex items-center gap-2 rounded-xl border border-white/5 bg-zinc-900/95 px-3 py-2 shadow-[0_8px_16px_-4px_rgba(0,0,0,0.3),0_4px_8px_-4px_rgba(0,0,0,0.2)]"
634
+ className="-translate-x-1/2 -translate-y-full absolute top-0 left-1/2"
635
+ style={{
636
+ backgroundColor: accentColor,
637
+ boxShadow: `0 0 12px ${accentColor}cc`,
638
+ height: lineHeight,
639
+ width: 2,
640
+ }}
641
+ />
642
+ <div
643
+ className="absolute top-0 left-1/2 flex h-8 w-8 items-center justify-center rounded-xl border border-white/5 bg-zinc-900/95 shadow-[0_8px_16px_-4px_rgba(0,0,0,0.3),0_4px_8px_-4px_rgba(0,0,0,0.2)]"
553
644
  style={{
554
645
  boxShadow: `0 8px 16px -4px rgba(0,0,0,0.3), 0 4px 8px -4px rgba(0,0,0,0.2), 0 0 18px ${accentColor}22`,
646
+ transform: `translate(-50%, calc(-100% - ${lineHeight}px))`,
555
647
  }}
556
648
  >
557
- <Icon
649
+ {/* eslint-disable-next-line @next/next/no-img-element */}
650
+ <img
651
+ alt=""
558
652
  aria-hidden="true"
559
- className="drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
560
- color={accentColor}
561
- height={16}
562
- icon={icon}
563
- width={16}
653
+ className="h-5 w-5 object-contain drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
654
+ src="/icons/paint.webp"
655
+ style={{
656
+ filter: state === 'ready' ? undefined : 'grayscale(1)',
657
+ opacity: iconOpacity,
658
+ }}
564
659
  />
565
- <span className="font-medium text-[11px]" style={{ color: accentColor }}>
566
- {label}
567
- </span>
660
+ {state === 'ready' ? (
661
+ isEraser ? (
662
+ <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)]">
663
+ <Icon
664
+ aria-hidden="true"
665
+ color="currentColor"
666
+ height={10}
667
+ icon="mdi:eraser-variant"
668
+ width={10}
669
+ />
670
+ </span>
671
+ ) : (
672
+ <span
673
+ 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)]"
674
+ style={{
675
+ backgroundColor: swatchColor,
676
+ backgroundImage: swatchImageUrl
677
+ ? `url(${JSON.stringify(swatchImageUrl)})`
678
+ : undefined,
679
+ }}
680
+ />
681
+ )
682
+ ) : state === 'blocked' ? (
683
+ <span className="-right-1 -bottom-1 absolute flex h-3.5 w-3.5 items-center justify-center rounded-full border border-white/30 bg-zinc-950 text-rose-300 shadow-[0_2px_6px_rgba(0,0,0,0.45)]">
684
+ <Icon
685
+ aria-hidden="true"
686
+ color="currentColor"
687
+ height={12}
688
+ icon="mdi:cancel"
689
+ width={12}
690
+ />
691
+ </span>
692
+ ) : (
693
+ <span className="-right-1 -bottom-1 absolute flex h-3.5 w-3.5 items-center justify-center rounded-full border border-white/30 bg-zinc-950 font-semibold text-[9px] text-slate-300 shadow-[0_2px_6px_rgba(0,0,0,0.45)]">
694
+ ?
695
+ </span>
696
+ )}
568
697
  </div>
569
698
  </div>
570
699
  )
571
700
  }
572
701
 
702
+ // Subscribes to `gridSnapStep` so the visible grid cell size matches whatever
703
+ // the wall draft tool snaps to — otherwise the cursor lands between visible
704
+ // grid lines when the user picks a finer snap (0.25 / 0.1 / 0.05).
705
+ function SnapAwareGrid() {
706
+ const gridSnapStep = useEditor((s) => s.gridSnapStep)
707
+ return <Grid cellColor="#aaa" cellSize={gridSnapStep} fadeDistance={500} sectionColor="#ccc" />
708
+ }
709
+
573
710
  // ── Viewer scene content: memoized so <Viewer> doesn't re-render on mode/viewMode changes ──
574
711
 
575
712
  const ViewerSceneContent = memo(function ViewerSceneContent({
576
713
  isVersionPreviewMode,
577
714
  isLoading,
578
715
  isFirstPersonMode,
716
+ isStudioMode,
579
717
  onThumbnailCapture,
580
718
  }: {
581
719
  isVersionPreviewMode: boolean
582
720
  isLoading: boolean
583
721
  isFirstPersonMode: boolean
722
+ isStudioMode: boolean
584
723
  onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
585
724
  }) {
725
+ // Studio mode is a clean render/snapshot surface — no selection or editing
726
+ // affordances. It mirrors version-preview's chrome gating on the canvas.
727
+ // Capture (snapshot) mode is camera-only for the same reason: suppress
728
+ // selection, editing handles, and the tool manager (which mounts the site
729
+ // boundary flags) so the framed shot stays clean.
730
+ const isCaptureMode = useEditor((s) => s.isCaptureMode)
731
+ const noEditing = isVersionPreviewMode || isFirstPersonMode || isStudioMode || isCaptureMode
586
732
  return (
587
733
  <>
588
- {!isFirstPersonMode && <SelectionManager />}
589
- {!(isVersionPreviewMode || isFirstPersonMode) && <BoxSelectTool />}
590
- {!(isVersionPreviewMode || isFirstPersonMode) && <FloatingActionMenu />}
591
- {!(isVersionPreviewMode || isFirstPersonMode) && <FloatingBuildingActionMenu />}
734
+ <SceneEnvironment />
735
+ {!(isFirstPersonMode || isStudioMode || isCaptureMode) && <SelectionManager />}
736
+ {!noEditing && <BoxSelectTool />}
737
+ {!noEditing && <NodeArrowHandles />}
738
+ {!noEditing && <GroupRotateHandle />}
739
+ {!noEditing && <GroupSelectionBox3D />}
740
+ {!noEditing && <WallOpeningHighlights />}
741
+ {!noEditing && <SlabHoleHighlights />}
742
+ {!noEditing && <WallMoveSideHandles />}
743
+ {!noEditing && <FenceTangentLines3D />}
744
+ {!noEditing && <FloatingActionMenu />}
745
+ {!noEditing && <GroupFloatingActionMenu />}
746
+ {!noEditing && <FloatingBuildingActionMenu />}
592
747
  {!isFirstPersonMode && <WallMeasurementLabel />}
593
748
  <ExportManager />
594
749
  {isFirstPersonMode ? <ViewerZoneSystem /> : <ZoneSystem />}
595
750
  <CeilingSystem />
596
751
  <CeilingSelectionAffordanceSystem />
752
+ {!noEditing && <SelectionAffordanceManager />}
597
753
  <RoofEditSystem />
598
754
  <StairEditSystem />
599
- {!(isLoading || isFirstPersonMode) && (
600
- <Grid cellColor="#aaa" fadeDistance={500} sectionColor="#ccc" />
601
- )}
602
- {!(isLoading || isVersionPreviewMode || isFirstPersonMode) && <ToolManager />}
755
+ {!(isLoading || isFirstPersonMode) && <SnapAwareGrid />}
756
+ {!(isLoading || noEditing) && <ToolManager />}
603
757
  {isFirstPersonMode && <FirstPersonControls />}
604
758
  <CustomCameraControls />
605
759
  <ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
606
- <PresetThumbnailGenerator />
607
760
  {!isFirstPersonMode && <SiteEdgeLabels />}
608
- {isFirstPersonMode && <InteractiveSystem />}
761
+ <InteractiveSystem />
609
762
  </>
610
763
  )
611
764
  })
@@ -678,7 +831,7 @@ function DeleteCursorLayer({
678
831
 
679
832
  return (
680
833
  <div
681
- className="pointer-events-none"
834
+ className="pointer-events-none z-40"
682
835
  ref={badgeRef}
683
836
  style={{ display: 'none', position: 'absolute', left: 0, top: 0 }}
684
837
  >
@@ -696,15 +849,15 @@ function PaintCursorLayer({
696
849
  }) {
697
850
  const mode = useEditor((s) => s.mode)
698
851
  const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
699
- const activePaintTarget = useEditor((s) => s.activePaintTarget)
700
- const badgeRef = useRef<HTMLDivElement>(null)
852
+ const paintEraser = useEditor((s) => s.paintEraser)
853
+ const paintHover = useEditor((s) => s.paintHover)
854
+ const sceneMaterials = useScene((s) => s.materials)
855
+ const [position, setPosition] = useState<{ x: number; y: number } | null>(null)
701
856
  const active = mode === 'material-paint' && !isVersionPreviewMode
702
857
 
703
858
  useEffect(() => {
704
859
  if (!active) {
705
- if (badgeRef.current) {
706
- badgeRef.current.style.display = 'none'
707
- }
860
+ setPosition(null)
708
861
  return
709
862
  }
710
863
  const el = containerRef.current
@@ -712,20 +865,16 @@ function PaintCursorLayer({
712
865
  let frame = 0
713
866
  let nextX = 0
714
867
  let nextY = 0
715
- const badge = badgeRef.current
716
868
 
717
869
  const flushPosition = () => {
718
870
  frame = 0
719
- if (!badge) return
720
- badge.style.display = 'block'
721
- badge.style.transform = `translate(${nextX + PAINT_CURSOR_BADGE_OFFSET_X}px, ${nextY + PAINT_CURSOR_BADGE_OFFSET_Y}px)`
871
+ setPosition({ x: nextX, y: nextY })
722
872
  }
723
873
 
724
- const onMove = (e: PointerEvent) => {
874
+ const updateFromEvent = (e: PointerEvent) => {
725
875
  const rect = el.getBoundingClientRect()
726
876
  nextX = e.clientX - rect.left
727
877
  nextY = e.clientY - rect.top
728
-
729
878
  if (frame === 0) {
730
879
  frame = window.requestAnimationFrame(flushPosition)
731
880
  }
@@ -735,48 +884,45 @@ function PaintCursorLayer({
735
884
  window.cancelAnimationFrame(frame)
736
885
  frame = 0
737
886
  }
738
- if (badge) {
739
- badge.style.display = 'none'
740
- }
887
+ setPosition(null)
741
888
  }
742
- el.addEventListener('pointermove', onMove)
889
+ el.addEventListener('pointermove', updateFromEvent)
890
+ el.addEventListener('pointerenter', updateFromEvent)
891
+ el.addEventListener('pointerdown', updateFromEvent)
743
892
  el.addEventListener('pointerleave', onLeave)
744
893
  return () => {
745
894
  if (frame !== 0) {
746
895
  window.cancelAnimationFrame(frame)
747
896
  }
748
- el.removeEventListener('pointermove', onMove)
897
+ el.removeEventListener('pointermove', updateFromEvent)
898
+ el.removeEventListener('pointerenter', updateFromEvent)
899
+ el.removeEventListener('pointerdown', updateFromEvent)
749
900
  el.removeEventListener('pointerleave', onLeave)
750
901
  }
751
902
  }, [active, containerRef])
752
903
 
753
- const hasMaterial = Boolean(
754
- activePaintMaterial &&
755
- (activePaintMaterial.material !== undefined ||
756
- activePaintMaterial.materialPreset !== undefined),
757
- )
758
- const label = hasMaterial ? `Paint ${activePaintTarget}` : 'Choose material'
759
- const icon = 'mdi:format-color-fill'
904
+ const hasPaint = paintEraser || hasActivePaintMaterial(activePaintMaterial)
905
+ const badgeState: PaintCursorBadgeState = !hasPaint
906
+ ? 'empty'
907
+ : paintHover != null
908
+ ? 'ready'
909
+ : 'blocked'
910
+ const swatchColor = getActivePaintMaterialSwatchColor(activePaintMaterial, sceneMaterials)
911
+ const swatchImageUrl = getActivePaintMaterialSwatchImageUrl(activePaintMaterial, sceneMaterials)
760
912
 
761
- useLayoutEffect(() => {
762
- if (!active && badgeRef.current) {
763
- badgeRef.current.style.display = 'none'
764
- }
765
- }, [active])
766
-
767
- if (!active) return null
913
+ if (!active || !position) return null
768
914
 
769
915
  return (
770
916
  <div
771
- className="pointer-events-none"
772
- ref={badgeRef}
773
- style={{ display: 'none', position: 'absolute', left: 0, top: 0 }}
917
+ className="pointer-events-none absolute z-40"
918
+ style={{ left: 0, top: 0, transform: `translate(${position.x}px, ${position.y}px)` }}
774
919
  >
775
920
  <PaintCursorBadge
776
- disabled={!hasMaterial}
777
- icon={icon}
778
- label={label}
921
+ isEraser={paintEraser}
779
922
  position={{ x: 0, y: 0 }}
923
+ state={badgeState}
924
+ swatchColor={swatchColor}
925
+ swatchImageUrl={swatchImageUrl}
780
926
  />
781
927
  </div>
782
928
  )
@@ -789,15 +935,21 @@ const ViewerCanvas = memo(function ViewerCanvas({
789
935
  isVersionPreviewMode,
790
936
  isLoading,
791
937
  isFirstPersonMode,
938
+ isStudioMode,
792
939
  hasLoadedInitialScene,
793
940
  showLoader,
941
+ sceneReadyKey,
942
+ onSceneReadyChange,
794
943
  onThumbnailCapture,
795
944
  }: {
796
945
  isVersionPreviewMode: boolean
797
946
  isLoading: boolean
798
947
  isFirstPersonMode: boolean
948
+ isStudioMode: boolean
799
949
  hasLoadedInitialScene: boolean
800
950
  showLoader: boolean
951
+ sceneReadyKey: number
952
+ onSceneReadyChange: (ready: boolean) => void
801
953
  onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
802
954
  }) {
803
955
  const viewMode = useEditor((s) => s.viewMode)
@@ -810,6 +962,13 @@ const ViewerCanvas = memo(function ViewerCanvas({
810
962
  )
811
963
 
812
964
  const viewerAreaRef = useRef<HTMLDivElement>(null)
965
+ // State mirror of `viewerAreaRef` so the floorplan compass portal re-renders
966
+ // once the container exists (a plain ref mutation wouldn't trigger it).
967
+ const [viewerAreaEl, setViewerAreaEl] = useState<HTMLDivElement | null>(null)
968
+ const setViewerAreaNode = useCallback((el: HTMLDivElement | null) => {
969
+ viewerAreaRef.current = el
970
+ setViewerAreaEl(el)
971
+ }, [])
813
972
  const viewer3dRef = useRef<HTMLDivElement>(null)
814
973
  const isResizingFloorplan = useRef(false)
815
974
 
@@ -839,7 +998,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
839
998
  window.removeEventListener('pointermove', handlePointerMove)
840
999
  window.removeEventListener('pointerup', handlePointerUp)
841
1000
  }
842
- }, [])
1001
+ }, [setFloorplanPaneRatio])
843
1002
 
844
1003
  useEffect(() => {
845
1004
  setIsCameraControlsHintVisible(!readCameraControlsHintDismissed())
@@ -855,7 +1014,10 @@ const ViewerCanvas = memo(function ViewerCanvas({
855
1014
 
856
1015
  return (
857
1016
  <ErrorBoundary fallback={<EditorSceneCrashFallback />}>
858
- <div className="flex h-full" ref={viewerAreaRef}>
1017
+ {/* `relative` so the floorplan compass (portaled here to stay visible in
1018
+ 2d / 3d / split alike) can anchor to this container's bottom-left. */}
1019
+ <div className="relative flex h-full" ref={setViewerAreaNode}>
1020
+ <QuickMeasurementHud />
859
1021
  {/* 2D floorplan — always mounted once shown, hidden via CSS to preserve state */}
860
1022
  <div
861
1023
  className="relative h-full flex-shrink-0"
@@ -865,7 +1027,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
865
1027
  }}
866
1028
  >
867
1029
  <div className="h-full w-full overflow-hidden">
868
- <FloorplanPanel />
1030
+ <FloorplanPanel compassHost={viewerAreaEl} />
869
1031
  </div>
870
1032
  {viewMode === 'split' && (
871
1033
  <div
@@ -880,6 +1042,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
880
1042
  {/* 3D viewer — always mounted, hidden via CSS to avoid destroying the WebGL context */}
881
1043
  <div
882
1044
  className="relative min-w-0 flex-1 overflow-hidden"
1045
+ data-pascal-viewer-3d
883
1046
  ref={viewer3dRef}
884
1047
  style={{ display: show3d ? undefined : 'none' }}
885
1048
  >
@@ -899,19 +1062,24 @@ const ViewerCanvas = memo(function ViewerCanvas({
899
1062
  ) : null}
900
1063
  <SelectionPersistenceManager enabled={hasLoadedInitialScene && !showLoader} />
901
1064
  <Viewer
1065
+ defaultRender={EDITOR_DEFAULT_RENDER}
902
1066
  hoverStyles={EDITOR_HOVER_STYLES}
1067
+ onSceneReadyChange={onSceneReadyChange}
1068
+ renderContext="editor"
1069
+ sceneReadyKey={sceneReadyKey}
903
1070
  selectionManager={isFirstPersonMode ? 'default' : 'custom'}
904
1071
  >
905
1072
  <ViewerSceneContent
906
1073
  isFirstPersonMode={isFirstPersonMode}
907
- isLoading={isLoading}
1074
+ isLoading={showLoader}
1075
+ isStudioMode={isStudioMode}
908
1076
  isVersionPreviewMode={isVersionPreviewMode}
909
1077
  onThumbnailCapture={onThumbnailCapture}
910
1078
  />
911
1079
  </Viewer>
912
1080
  </div>
913
1081
  </div>
914
- {!(isLoading || isVersionPreviewMode) && <ZoneLabelEditorSystem />}
1082
+ {!(showLoader || isVersionPreviewMode) && <ZoneLabelEditorSystem />}
915
1083
  </ErrorBoundary>
916
1084
  )
917
1085
  })
@@ -924,6 +1092,8 @@ export default function Editor({
924
1092
  sidebarTabs,
925
1093
  viewerToolbarLeft,
926
1094
  viewerToolbarRight,
1095
+ stageOverlay,
1096
+ inspectorFooter,
927
1097
  projectId,
928
1098
  onLoad,
929
1099
  onSave,
@@ -932,18 +1102,19 @@ export default function Editor({
932
1102
  previewScene,
933
1103
  isVersionPreviewMode = false,
934
1104
  isLoading = false,
1105
+ onLoaderChange,
935
1106
  onThumbnailCapture,
936
1107
  sidebarOverlay,
937
1108
  viewerBanner,
938
1109
  settingsPanelProps,
939
1110
  sitePanelProps,
940
1111
  extraSidebarPanels,
941
- presetsAdapter,
942
1112
  commandPaletteEmptyAction,
943
1113
  }: EditorProps) {
944
1114
  const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
1115
+ const isStudioMode = useEditor((s) => s.workspaceMode === 'studio')
945
1116
 
946
- useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode })
1117
+ useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode || isStudioMode })
947
1118
 
948
1119
  const { isLoadingSceneRef } = useAutoSave({
949
1120
  onSave,
@@ -954,17 +1125,30 @@ export default function Editor({
954
1125
 
955
1126
  const [isSceneLoading, setIsSceneLoading] = useState(false)
956
1127
  const [hasLoadedInitialScene, setHasLoadedInitialScene] = useState(false)
1128
+ const [sceneReadyKey, setSceneReadyKey] = useState(0)
1129
+ const [isViewerSceneReady, setIsViewerSceneReady] = useState(false)
957
1130
  const isPreviewMode = useEditor((s) => s.isPreviewMode)
958
1131
  const isCaptureMode = useEditor((s) => s.isCaptureMode)
959
1132
 
960
1133
  const sidebarWidth = useSidebarStore((s) => s.width)
961
1134
  const isSidebarCollapsed = useSidebarStore((s) => s.isCollapsed)
962
1135
 
1136
+ // Host-registered rail panels. Called unconditionally so
1137
+ // hook order is stable across the v1 / v2 layout branches below; the v1
1138
+ // AppSidebar path merges its own copy internally, the v2 path merges these
1139
+ // into its tab bar.
1140
+ const hostRailPanels = useHostPanels()
1141
+
963
1142
  useEffect(() => {
964
1143
  const teardown = initializeEditorRuntime()
965
1144
  return teardown
966
1145
  }, [])
967
1146
 
1147
+ useEffect(() => {
1148
+ void useEditor.persist.rehydrate()
1149
+ void useSidebarStore.persist.rehydrate()
1150
+ }, [])
1151
+
968
1152
  useEffect(() => {
969
1153
  useViewer.getState().setProjectId(projectId ?? null)
970
1154
 
@@ -980,15 +1164,24 @@ export default function Editor({
980
1164
  async function load() {
981
1165
  isLoadingSceneRef.current = true
982
1166
  setHasLoadedInitialScene(false)
1167
+ setIsViewerSceneReady(false)
983
1168
  setIsSceneLoading(true)
1169
+ useScene.getState().unloadScene()
1170
+ useViewer.getState().resetSelection()
984
1171
 
985
1172
  try {
986
1173
  const sceneGraph = onLoad ? await onLoad() : loadSceneFromLocalStorage()
987
1174
  if (!cancelled) {
988
1175
  applySceneGraphToEditor(sceneGraph)
1176
+ setIsViewerSceneReady(false)
1177
+ setSceneReadyKey((key) => key + 1)
989
1178
  }
990
1179
  } catch {
991
- if (!cancelled) applySceneGraphToEditor(null)
1180
+ if (!cancelled) {
1181
+ applySceneGraphToEditor(null)
1182
+ setIsViewerSceneReady(false)
1183
+ setSceneReadyKey((key) => key + 1)
1184
+ }
992
1185
  } finally {
993
1186
  if (!cancelled) {
994
1187
  setIsSceneLoading(false)
@@ -1032,7 +1225,28 @@ export default function Editor({
1032
1225
  }
1033
1226
  }, [])
1034
1227
 
1035
- const showLoader = isLoading || isSceneLoading
1228
+ const handleSceneReadyChange = useCallback((ready: boolean) => {
1229
+ setIsViewerSceneReady(ready)
1230
+ }, [])
1231
+
1232
+ useEffect(() => {
1233
+ if (isLoading || isSceneLoading || !hasLoadedInitialScene || isViewerSceneReady) return
1234
+
1235
+ const timer = window.setTimeout(() => {
1236
+ console.warn('[editor] viewer scene readiness timed out; showing editor shell anyway', {
1237
+ sceneReadyKey,
1238
+ })
1239
+ setIsViewerSceneReady(true)
1240
+ }, SCENE_READY_FALLBACK_MS)
1241
+
1242
+ return () => window.clearTimeout(timer)
1243
+ }, [hasLoadedInitialScene, isLoading, isSceneLoading, isViewerSceneReady, sceneReadyKey])
1244
+
1245
+ const showLoader = isLoading || isSceneLoading || !hasLoadedInitialScene || !isViewerSceneReady
1246
+
1247
+ useEffect(() => {
1248
+ onLoaderChange?.(showLoader)
1249
+ }, [showLoader, onLoaderChange])
1036
1250
 
1037
1251
  const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
1038
1252
  const wasFirstPersonModeRef = useRef(isFirstPersonMode)
@@ -1071,15 +1285,20 @@ export default function Editor({
1071
1285
  }, [isFirstPersonMode])
1072
1286
 
1073
1287
  const previewViewerContent = (
1074
- <Viewer hoverStyles={EDITOR_HOVER_STYLES} selectionManager="default">
1288
+ <Viewer
1289
+ defaultRender={EDITOR_DEFAULT_RENDER}
1290
+ hoverStyles={EDITOR_HOVER_STYLES}
1291
+ renderContext="editor"
1292
+ selectionManager="default"
1293
+ >
1075
1294
  <ExportManager />
1076
1295
  <ViewerZoneSystem />
1077
1296
  <CeilingSystem />
1078
1297
  <RoofEditSystem />
1079
1298
  <StairEditSystem />
1299
+ {isFirstPersonMode && <FirstPersonControls />}
1080
1300
  <CustomCameraControls />
1081
1301
  <ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
1082
- <PresetThumbnailGenerator />
1083
1302
  <InteractiveSystem />
1084
1303
  </Viewer>
1085
1304
  )
@@ -1089,15 +1308,27 @@ export default function Editor({
1089
1308
  hasLoadedInitialScene={hasLoadedInitialScene}
1090
1309
  isFirstPersonMode={isFirstPersonMode}
1091
1310
  isLoading={isLoading}
1311
+ isStudioMode={isStudioMode}
1092
1312
  isVersionPreviewMode={isVersionPreviewMode}
1313
+ onSceneReadyChange={handleSceneReadyChange}
1093
1314
  onThumbnailCapture={onThumbnailCapture}
1315
+ sceneReadyKey={sceneReadyKey}
1094
1316
  showLoader={showLoader}
1095
1317
  />
1096
1318
  )
1097
1319
 
1098
1320
  // ── V2 layout ──
1099
1321
  if (layoutVersion === 'v2') {
1100
- const tabMap = new Map(sidebarTabs?.map((t) => [t.id, t]) ?? [])
1322
+ // Registered host panels join the host's `sidebarTabs` as first-class tabs.
1323
+ // Explicit tabs keep precedence because they are already in the map first.
1324
+ const tabMap = new Map<string, SidebarTab & { component: React.ComponentType }>(
1325
+ sidebarTabs?.map((t) => [t.id, t]) ?? [],
1326
+ )
1327
+ for (const p of hostRailPanels) {
1328
+ if (!tabMap.has(p.id)) {
1329
+ tabMap.set(p.id, { id: p.id, label: p.label, icon: p.icon, component: p.component })
1330
+ }
1331
+ }
1101
1332
 
1102
1333
  const renderTabContent = (tabId: string) => {
1103
1334
  // Built-in panels
@@ -1114,26 +1345,43 @@ export default function Editor({
1114
1345
  return <Component />
1115
1346
  }
1116
1347
 
1117
- const tabBarTabs =
1118
- sidebarTabs?.map(({ id, label, mobileDefaultSnap, mobileIcon }) => ({
1348
+ const tabBarTabs = [
1349
+ ...(sidebarTabs?.map(({ id, label, mobileDefaultSnap, mobileIcon, icon }) => ({
1119
1350
  id,
1120
1351
  label,
1121
1352
  mobileDefaultSnap,
1122
1353
  mobileIcon,
1123
- })) ?? []
1354
+ icon,
1355
+ })) ?? []),
1356
+ // Host panels appear after the explicit tabs in the rail. The icon
1357
+ // doubles as the mobile icon; a half-height sheet is a sensible default.
1358
+ ...hostRailPanels.map((p) => ({
1359
+ id: p.id,
1360
+ label: p.label,
1361
+ mobileDefaultSnap: 0.5,
1362
+ mobileIcon: p.icon,
1363
+ icon: p.icon,
1364
+ })),
1365
+ ]
1124
1366
 
1125
1367
  return (
1126
- <PresetsProvider adapter={presetsAdapter}>
1368
+ <>
1127
1369
  {showLoader && (
1128
1370
  <div className="fixed inset-0 z-60">
1129
- <SceneLoader />
1371
+ <SceneLoader className="bg-background" />
1130
1372
  </div>
1131
1373
  )}
1132
1374
 
1133
1375
  {!isLoading && isPreviewMode ? (
1134
1376
  <div className="dark flex h-full w-full flex-col bg-neutral-100 text-foreground">
1135
- <ViewerOverlay onBack={() => useEditor.getState().setPreviewMode(false)} />
1136
- <div className="h-full w-full">{previewViewerContent}</div>
1377
+ {isFirstPersonMode ? (
1378
+ <FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
1379
+ ) : (
1380
+ <ViewerOverlay onBack={() => useEditor.getState().setPreviewMode(false)} />
1381
+ )}
1382
+ <div className="h-full w-full" data-pascal-viewer-3d>
1383
+ {previewViewerContent}
1384
+ </div>
1137
1385
  </div>
1138
1386
  ) : (
1139
1387
  <>
@@ -1141,15 +1389,15 @@ export default function Editor({
1141
1389
  navbarSlot={navbarSlot}
1142
1390
  overlays={
1143
1391
  <>
1144
- {!isCaptureMode && <FloatingLevelSelector />}
1145
- {!(isVersionPreviewMode || isCaptureMode) && (
1392
+ {!(isCaptureMode || stageOverlay) && <FloatingLevelSelector />}
1393
+ {!(isVersionPreviewMode || isCaptureMode || isStudioMode) && (
1146
1394
  <div className="pointer-events-auto">
1147
1395
  <ActionMenu />
1148
1396
  </div>
1149
1397
  )}
1150
- {!(isVersionPreviewMode || isCaptureMode) && (
1398
+ {!(isVersionPreviewMode || isCaptureMode || isStudioMode) && (
1151
1399
  <div className="pointer-events-auto">
1152
- <PanelManager />
1400
+ <PanelManager inspectorFooter={inspectorFooter} />
1153
1401
  </div>
1154
1402
  )}
1155
1403
  {!isCaptureMode && (
@@ -1157,6 +1405,11 @@ export default function Editor({
1157
1405
  <HelperManager />
1158
1406
  </div>
1159
1407
  )}
1408
+ {isFirstPersonMode && (
1409
+ <FirstPersonOverlay
1410
+ onExit={() => useEditor.getState().setFirstPersonMode(false)}
1411
+ />
1412
+ )}
1160
1413
  {viewerBanner}
1161
1414
  {projectId ? <SnapshotCaptureOverlay projectId={projectId} /> : null}
1162
1415
  </>
@@ -1164,21 +1417,16 @@ export default function Editor({
1164
1417
  renderTabContent={renderTabContent}
1165
1418
  sidebarOverlay={sidebarOverlay}
1166
1419
  sidebarTabs={tabBarTabs}
1420
+ stageOverlay={stageOverlay}
1167
1421
  viewerContent={viewerCanvas}
1168
1422
  viewerToolbarLeft={viewerToolbarLeft}
1169
1423
  viewerToolbarRight={viewerToolbarRight}
1170
1424
  />
1171
1425
  <EditorCommands />
1172
1426
  <CommandPalette emptyAction={commandPaletteEmptyAction} />
1173
- {/* First-person overlay — rendered on top of normal layout */}
1174
- {isFirstPersonMode && (
1175
- <div className="pointer-events-none fixed inset-0 z-50">
1176
- <FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
1177
- </div>
1178
- )}
1179
1427
  </>
1180
1428
  )}
1181
- </PresetsProvider>
1429
+ </>
1182
1430
  )
1183
1431
  }
1184
1432
 
@@ -1189,57 +1437,59 @@ export default function Editor({
1189
1437
  const overlayLeft = LAYOUT_PADDING + (isSidebarCollapsed ? 8 : sidebarWidth) + LAYOUT_GAP
1190
1438
 
1191
1439
  return (
1192
- <PresetsProvider adapter={presetsAdapter}>
1193
- <div className="dark flex h-full w-full gap-3 bg-neutral-100 p-3 text-foreground">
1194
- {showLoader && (
1195
- <div className="fixed inset-0 z-60">
1196
- <SceneLoader />
1197
- </div>
1198
- )}
1440
+ <div className="dark flex h-full w-full gap-3 bg-neutral-100 p-3 text-foreground">
1441
+ {showLoader && (
1442
+ <div className="fixed inset-0 z-60">
1443
+ <SceneLoader className="bg-background" />
1444
+ </div>
1445
+ )}
1199
1446
 
1200
- {!isLoading && isPreviewMode ? (
1201
- <>
1447
+ {!isLoading && isPreviewMode ? (
1448
+ <>
1449
+ {isFirstPersonMode ? (
1450
+ <FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
1451
+ ) : (
1202
1452
  <ViewerOverlay onBack={() => useEditor.getState().setPreviewMode(false)} />
1203
- <div className="h-full w-full">{previewViewerContent}</div>
1204
- </>
1205
- ) : (
1206
- <>
1207
- {/* Sidebar */}
1208
- <SidebarSlot>
1209
- <AppSidebar
1210
- appMenuButton={appMenuButton}
1211
- commandPaletteEmptyAction={commandPaletteEmptyAction}
1212
- extraPanels={extraSidebarPanels}
1213
- settingsPanelProps={settingsPanelProps}
1214
- sidebarTop={sidebarTop}
1215
- sitePanelProps={sitePanelProps}
1216
- />
1217
- </SidebarSlot>
1218
-
1219
- {/* Viewer area */}
1220
- <div className="relative flex-1 overflow-hidden rounded-xl">{viewerCanvas}</div>
1221
-
1222
- {/* Fixed UI overlays scoped to the viewer area */}
1223
- <ViewerOverlays left={overlayLeft}>
1224
- <div className="pointer-events-auto">
1225
- <ActionMenu />
1226
- </div>
1227
- <div className="pointer-events-auto">
1228
- <PanelManager />
1229
- </div>
1230
- <div className="pointer-events-auto">
1231
- <HelperManager />
1232
- </div>
1233
- </ViewerOverlays>
1234
- {/* First-person overlay — rendered on top of normal layout */}
1453
+ )}
1454
+ <div className="h-full w-full" data-pascal-viewer-3d>
1455
+ {previewViewerContent}
1456
+ </div>
1457
+ </>
1458
+ ) : (
1459
+ <>
1460
+ {/* Sidebar */}
1461
+ <SidebarSlot>
1462
+ <AppSidebar
1463
+ appMenuButton={appMenuButton}
1464
+ commandPaletteEmptyAction={commandPaletteEmptyAction}
1465
+ extraPanels={extraSidebarPanels}
1466
+ settingsPanelProps={settingsPanelProps}
1467
+ sidebarTop={sidebarTop}
1468
+ sitePanelProps={sitePanelProps}
1469
+ />
1470
+ </SidebarSlot>
1471
+
1472
+ {/* Viewer area */}
1473
+ <div className="relative flex-1 overflow-hidden rounded-xl">{viewerCanvas}</div>
1474
+
1475
+ {/* Fixed UI overlays scoped to the viewer area */}
1476
+ <ViewerOverlays left={overlayLeft}>
1477
+ <div className="pointer-events-auto">
1478
+ <ActionMenu />
1479
+ </div>
1480
+ <div className="pointer-events-auto">
1481
+ <PanelManager />
1482
+ </div>
1483
+ <div className="pointer-events-auto">
1484
+ <HelperManager />
1485
+ </div>
1486
+ <RiserDiagramPanel />
1235
1487
  {isFirstPersonMode && (
1236
- <div className="pointer-events-none fixed inset-0 z-50">
1237
- <FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
1238
- </div>
1488
+ <FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
1239
1489
  )}
1240
- </>
1241
- )}
1242
- </div>
1243
- </PresetsProvider>
1490
+ </ViewerOverlays>
1491
+ </>
1492
+ )}
1493
+ </div>
1244
1494
  )
1245
1495
  }