@pascal-app/editor 0.9.1 → 1.0.0-beta.1

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 (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -2,28 +2,41 @@
2
2
 
3
3
  import { Icon } from '@iconify/react'
4
4
  import {
5
+ acquireSceneReadOnlyLease,
6
+ getCatalogMaterialById,
7
+ getLibraryMaterialIdFromRef,
8
+ getSceneMaterialIdFromRef,
5
9
  initSpaceDetectionSync,
6
10
  initSpatialGridSync,
7
11
  spatialGridManager,
8
12
  useScene,
9
13
  } from '@pascal-app/core'
10
- import { type HoverStyles, InteractiveSystem, useViewer, Viewer } from '@pascal-app/viewer'
14
+ import {
15
+ type HoverStyles,
16
+ InteractiveSystem,
17
+ SceneEnvironment,
18
+ useViewer,
19
+ Viewer,
20
+ } from '@pascal-app/viewer'
11
21
  import { memo, type ReactNode, useCallback, useEffect, useRef, useState } from 'react'
12
22
  import { ViewerOverlay } from '../../components/viewer-overlay'
13
23
  import { ViewerZoneSystem } from '../../components/viewer-zone-system'
14
24
  import { type SaveStatus, useAutoSave } from '../../hooks/use-auto-save'
15
25
  import { useKeyboard } from '../../hooks/use-keyboard'
26
+ import { type ActivePaintMaterial, hasActivePaintMaterial } from '../../lib/material-paint'
16
27
  import {
17
28
  applySceneGraphToEditor,
18
29
  loadSceneFromLocalStorage,
19
30
  type SceneGraph,
20
31
  writePersistedSelection,
21
32
  } from '../../lib/scene'
22
- import { initSFXBus } from '../../lib/sfx-bus'
33
+ import { disposeSFXBus, initSFXBus } from '../../lib/sfx-bus'
23
34
  import useEditor from '../../store/use-editor'
35
+ import useFloorplanMode from '../../store/use-floorplan-mode'
24
36
  import { CeilingSelectionAffordanceSystem } from '../systems/ceiling/ceiling-selection-affordance-system'
25
37
  import { CeilingSystem } from '../systems/ceiling/ceiling-system'
26
38
  import { RoofEditSystem } from '../systems/roof/roof-edit-system'
39
+ import { SelectionAffordanceManager } from '../systems/selection-affordance-manager'
27
40
  import { StairEditSystem } from '../systems/stair/stair-edit-system'
28
41
  import { ZoneLabelEditorSystem } from '../systems/zone/zone-label-editor-system'
29
42
  import { ZoneSystem } from '../systems/zone/zone-system'
@@ -44,17 +57,24 @@ import type { ExtraPanel } from '../ui/sidebar/icon-rail'
44
57
  import { SettingsPanel, type SettingsPanelProps } from '../ui/sidebar/panels/settings-panel'
45
58
  import { SitePanel, type SitePanelProps } from '../ui/sidebar/panels/site-panel'
46
59
  import type { SidebarTab } from '../ui/sidebar/tab-bar'
60
+ import { useHostPanels } from '../ui/sidebar/use-plugin-panels'
47
61
  import { CustomCameraControls } from './custom-camera-controls'
62
+ import { DeleteConfirmationDialog } from './delete-confirmation-dialog'
48
63
  import { EditorLayoutV2 } from './editor-layout-v2'
49
64
  import { ExportManager } from './export-manager'
65
+ import { FenceTangentLines3D } from './fence-tangent-lines-3d'
50
66
  import { FirstPersonControls, FirstPersonOverlay } from './first-person-controls'
51
67
  import { FloatingActionMenu } from './floating-action-menu'
52
68
  import { FloatingBuildingActionMenu } from './floating-building-action-menu'
69
+ import { FloorplanModeCoordinator } from './floorplan-mode-coordinator'
53
70
  import { FloorplanPanel } from './floorplan-panel'
54
71
  import { Grid } from './grid'
55
- import { GroupMoveHandle } from './group-move-handle'
72
+ import { GroupFloatingActionMenu } from './group-floating-action-menu'
56
73
  import { GroupRotateHandle } from './group-rotate-handle'
74
+ import { GroupSelectionBox3D } from './group-selection-box-3d'
57
75
  import { NodeArrowHandles } from './node-arrow-handles'
76
+ import { QuickMeasurementHud } from './quick-measurement-hud'
77
+ import { RiserDiagramPanel } from './riser-diagram-panel'
58
78
  import { SelectionManager } from './selection-manager'
59
79
  import { SiteEdgeLabels } from './site-edge-labels'
60
80
  import { SlabHoleHighlights } from './slab-hole-highlights'
@@ -72,6 +92,8 @@ const PAINT_CURSOR_BADGE_COLOR = '#818cf8'
72
92
  const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
73
93
  const PAINT_CURSOR_BADGE_OFFSET_X = 14
74
94
  const PAINT_CURSOR_BADGE_OFFSET_Y = 14
95
+ const SCENE_READY_FALLBACK_MS = 8000
96
+ type PaintCursorBadgeState = 'empty' | 'ready' | 'blocked'
75
97
  const EDITOR_HOVER_STYLES: HoverStyles = {
76
98
  default: { visibleColor: 0x00_aa_ff, hiddenColor: 0xf3_ff_47, strength: 5, pulse: true },
77
99
  delete: { visibleColor: 0xef_44_44, hiddenColor: 0x99_1b_1b, strength: 6, pulse: false },
@@ -102,6 +124,7 @@ function initializeEditorRuntime(): () => void {
102
124
  unsubscribeSpaceDetection?.()
103
125
 
104
126
  spatialGridManager.clear()
127
+ disposeSFXBus()
105
128
 
106
129
  const outliner = useViewer.getState().outliner
107
130
  outliner.selectedObjects.length = 0
@@ -121,12 +144,29 @@ export interface EditorProps {
121
144
  sidebarTabs?: (SidebarTab & { component: React.ComponentType })[]
122
145
  viewerToolbarLeft?: ReactNode
123
146
  viewerToolbarRight?: ReactNode
147
+ /**
148
+ * Full-bleed surface swapped in over the 3D canvas (v2) — e.g. the studio
149
+ * gallery. The canvas stays mounted underneath (no WebGL re-init) and the
150
+ * viewer toolbar stays on top so the host's stage switch remains reachable.
151
+ */
152
+ stageOverlay?: ReactNode
124
153
  /**
125
154
  * Docked below the node inspector (v2). Hosts mount the "save as preset"
126
155
  * affordance here so it reads as part of the inspector surface and shows
127
156
  * only while a node is selected.
128
157
  */
129
158
  inspectorFooter?: ReactNode
159
+ /**
160
+ * Docked below the multi-selection panel (v2). Hosts mount whole-selection
161
+ * affordances here (e.g. "Save to my catalog"); shows only while more than
162
+ * one node is selected.
163
+ */
164
+ multiSelectionFooter?: ReactNode
165
+
166
+ /** Host-owned content mounted inside the editor's React Three Fiber scene. */
167
+ viewerSceneSlot?: ReactNode
168
+ /** Host-owned SVG content mounted in the transformed floor-plan scene. */
169
+ floorplanSceneSlot?: ReactNode
130
170
 
131
171
  projectId?: string | null
132
172
 
@@ -143,6 +183,10 @@ export interface EditorProps {
143
183
  // Loading indicator (e.g. project fetching in community mode)
144
184
  isLoading?: boolean
145
185
 
186
+ // Fires when the full-screen scene loader shows/hides — lets hosts measure
187
+ // open-to-interactive time without reaching into internal loader state.
188
+ onLoaderChange?: (visible: boolean) => void
189
+
146
190
  // Thumbnail
147
191
  onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
148
192
 
@@ -526,14 +570,70 @@ function DeleteCursorBadge({ position }: { position: { x: number; y: number } })
526
570
  )
527
571
  }
528
572
 
573
+ function getActivePaintMaterialSwatchColor(
574
+ material: ActivePaintMaterial | null,
575
+ sceneMaterials: ReturnType<typeof useScene.getState>['materials'],
576
+ ) {
577
+ const directColor = material?.material?.properties?.color
578
+ if (directColor) return directColor
579
+
580
+ const sceneMaterialId = getSceneMaterialIdFromRef(material?.materialPreset)
581
+ if (sceneMaterialId) {
582
+ const sceneMaterial = sceneMaterials[sceneMaterialId as keyof typeof sceneMaterials]
583
+ const sceneColor = sceneMaterial?.material.properties?.color
584
+ if (sceneColor) return sceneColor
585
+ }
586
+
587
+ const catalogId =
588
+ getLibraryMaterialIdFromRef(material?.materialPreset) ?? material?.material?.id ?? undefined
589
+ const catalogMaterial = getCatalogMaterialById(catalogId)
590
+ return (
591
+ catalogMaterial?.previewColor ??
592
+ catalogMaterial?.preset.mapProperties.color ??
593
+ PAINT_CURSOR_BADGE_COLOR
594
+ )
595
+ }
596
+
597
+ function getActivePaintMaterialSwatchImageUrl(
598
+ material: ActivePaintMaterial | null,
599
+ sceneMaterials: ReturnType<typeof useScene.getState>['materials'],
600
+ ) {
601
+ const directTextureUrl = material?.material?.texture?.url
602
+ if (directTextureUrl) return directTextureUrl
603
+
604
+ const sceneMaterialId = getSceneMaterialIdFromRef(material?.materialPreset)
605
+ if (sceneMaterialId) {
606
+ const sceneMaterial = sceneMaterials[sceneMaterialId as keyof typeof sceneMaterials]
607
+ const sceneTextureUrl = sceneMaterial?.material.texture?.url
608
+ if (sceneTextureUrl) return sceneTextureUrl
609
+ }
610
+
611
+ const catalogId =
612
+ getLibraryMaterialIdFromRef(material?.materialPreset) ?? material?.material?.id ?? undefined
613
+ const catalogMaterial = getCatalogMaterialById(catalogId)
614
+ return catalogMaterial?.previewThumbnailUrl ?? catalogMaterial?.preset.maps.albedoMap
615
+ }
616
+
529
617
  function PaintCursorBadge({
530
618
  position,
531
- disabled,
619
+ state,
620
+ swatchColor,
621
+ swatchImageUrl,
622
+ isEraser,
532
623
  }: {
533
624
  position: { x: number; y: number }
534
- disabled: boolean
625
+ state: PaintCursorBadgeState
626
+ swatchColor: string
627
+ swatchImageUrl?: string
628
+ isEraser: boolean
535
629
  }) {
536
- const accentColor = disabled ? PAINT_CURSOR_BADGE_DISABLED_COLOR : PAINT_CURSOR_BADGE_COLOR
630
+ const accentColor =
631
+ state === 'ready'
632
+ ? isEraser
633
+ ? PAINT_CURSOR_BADGE_COLOR
634
+ : swatchColor
635
+ : PAINT_CURSOR_BADGE_DISABLED_COLOR
636
+ const iconOpacity = state === 'ready' ? 1 : state === 'blocked' ? 0.62 : 0.42
537
637
  const lineHeight = 18
538
638
 
539
639
  return (
@@ -566,8 +666,49 @@ function PaintCursorBadge({
566
666
  alt=""
567
667
  aria-hidden="true"
568
668
  className="h-5 w-5 object-contain drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
569
- src="/icons/paint.png"
669
+ src="/icons/paint.webp"
670
+ style={{
671
+ filter: state === 'ready' ? undefined : 'grayscale(1)',
672
+ opacity: iconOpacity,
673
+ }}
570
674
  />
675
+ {state === 'ready' ? (
676
+ isEraser ? (
677
+ <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)]">
678
+ <Icon
679
+ aria-hidden="true"
680
+ color="currentColor"
681
+ height={10}
682
+ icon="mdi:eraser-variant"
683
+ width={10}
684
+ />
685
+ </span>
686
+ ) : (
687
+ <span
688
+ 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)]"
689
+ style={{
690
+ backgroundColor: swatchColor,
691
+ backgroundImage: swatchImageUrl
692
+ ? `url(${JSON.stringify(swatchImageUrl)})`
693
+ : undefined,
694
+ }}
695
+ />
696
+ )
697
+ ) : state === 'blocked' ? (
698
+ <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)]">
699
+ <Icon
700
+ aria-hidden="true"
701
+ color="currentColor"
702
+ height={12}
703
+ icon="mdi:cancel"
704
+ width={12}
705
+ />
706
+ </span>
707
+ ) : (
708
+ <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)]">
709
+ ?
710
+ </span>
711
+ )}
571
712
  </div>
572
713
  </div>
573
714
  )
@@ -589,33 +730,43 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
589
730
  isFirstPersonMode,
590
731
  isStudioMode,
591
732
  onThumbnailCapture,
733
+ viewerSceneSlot,
592
734
  }: {
593
735
  isVersionPreviewMode: boolean
594
736
  isLoading: boolean
595
737
  isFirstPersonMode: boolean
596
738
  isStudioMode: boolean
597
739
  onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
740
+ viewerSceneSlot?: ReactNode
598
741
  }) {
599
742
  // Studio mode is a clean render/snapshot surface — no selection or editing
600
743
  // affordances. It mirrors version-preview's chrome gating on the canvas.
601
- const noEditing = isVersionPreviewMode || isFirstPersonMode || isStudioMode
744
+ // Capture (snapshot) mode is camera-only for the same reason: suppress
745
+ // selection, editing handles, and the tool manager (which mounts the site
746
+ // boundary flags) so the framed shot stays clean.
747
+ const isCaptureMode = useEditor((s) => s.isCaptureMode)
748
+ const noEditing = isVersionPreviewMode || isFirstPersonMode || isStudioMode || isCaptureMode
602
749
  return (
603
750
  <>
604
- {!(isFirstPersonMode || isStudioMode) && <SelectionManager />}
751
+ <SceneEnvironment />
752
+ {!(isFirstPersonMode || isStudioMode || isCaptureMode) && <SelectionManager />}
605
753
  {!noEditing && <BoxSelectTool />}
606
754
  {!noEditing && <NodeArrowHandles />}
607
755
  {!noEditing && <GroupRotateHandle />}
608
- {!noEditing && <GroupMoveHandle />}
756
+ {!noEditing && <GroupSelectionBox3D />}
609
757
  {!noEditing && <WallOpeningHighlights />}
610
758
  {!noEditing && <SlabHoleHighlights />}
611
759
  {!noEditing && <WallMoveSideHandles />}
760
+ {!noEditing && <FenceTangentLines3D />}
612
761
  {!noEditing && <FloatingActionMenu />}
762
+ {!noEditing && <GroupFloatingActionMenu />}
613
763
  {!noEditing && <FloatingBuildingActionMenu />}
614
764
  {!isFirstPersonMode && <WallMeasurementLabel />}
615
765
  <ExportManager />
616
766
  {isFirstPersonMode ? <ViewerZoneSystem /> : <ZoneSystem />}
617
767
  <CeilingSystem />
618
768
  <CeilingSelectionAffordanceSystem />
769
+ {!noEditing && <SelectionAffordanceManager />}
619
770
  <RoofEditSystem />
620
771
  <StairEditSystem />
621
772
  {!(isLoading || isFirstPersonMode) && <SnapAwareGrid />}
@@ -625,6 +776,7 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
625
776
  <ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
626
777
  {!isFirstPersonMode && <SiteEdgeLabels />}
627
778
  <InteractiveSystem />
779
+ {!noEditing && viewerSceneSlot}
628
780
  </>
629
781
  )
630
782
  })
@@ -715,6 +867,9 @@ function PaintCursorLayer({
715
867
  }) {
716
868
  const mode = useEditor((s) => s.mode)
717
869
  const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
870
+ const paintEraser = useEditor((s) => s.paintEraser)
871
+ const paintHover = useEditor((s) => s.paintHover)
872
+ const sceneMaterials = useScene((s) => s.materials)
718
873
  const [position, setPosition] = useState<{ x: number; y: number } | null>(null)
719
874
  const active = mode === 'material-paint' && !isVersionPreviewMode
720
875
 
@@ -764,11 +919,14 @@ function PaintCursorLayer({
764
919
  }
765
920
  }, [active, containerRef])
766
921
 
767
- const hasMaterial = Boolean(
768
- activePaintMaterial &&
769
- (activePaintMaterial.material !== undefined ||
770
- activePaintMaterial.materialPreset !== undefined),
771
- )
922
+ const hasPaint = paintEraser || hasActivePaintMaterial(activePaintMaterial)
923
+ const badgeState: PaintCursorBadgeState = !hasPaint
924
+ ? 'empty'
925
+ : paintHover != null
926
+ ? 'ready'
927
+ : 'blocked'
928
+ const swatchColor = getActivePaintMaterialSwatchColor(activePaintMaterial, sceneMaterials)
929
+ const swatchImageUrl = getActivePaintMaterialSwatchImageUrl(activePaintMaterial, sceneMaterials)
772
930
 
773
931
  if (!active || !position) return null
774
932
 
@@ -777,7 +935,13 @@ function PaintCursorLayer({
777
935
  className="pointer-events-none absolute z-40"
778
936
  style={{ left: 0, top: 0, transform: `translate(${position.x}px, ${position.y}px)` }}
779
937
  >
780
- <PaintCursorBadge disabled={!hasMaterial} position={{ x: 0, y: 0 }} />
938
+ <PaintCursorBadge
939
+ isEraser={paintEraser}
940
+ position={{ x: 0, y: 0 }}
941
+ state={badgeState}
942
+ swatchColor={swatchColor}
943
+ swatchImageUrl={swatchImageUrl}
944
+ />
781
945
  </div>
782
946
  )
783
947
  }
@@ -795,6 +959,8 @@ const ViewerCanvas = memo(function ViewerCanvas({
795
959
  sceneReadyKey,
796
960
  onSceneReadyChange,
797
961
  onThumbnailCapture,
962
+ viewerSceneSlot,
963
+ floorplanSceneSlot,
798
964
  }: {
799
965
  isVersionPreviewMode: boolean
800
966
  isLoading: boolean
@@ -805,6 +971,8 @@ const ViewerCanvas = memo(function ViewerCanvas({
805
971
  sceneReadyKey: number
806
972
  onSceneReadyChange: (ready: boolean) => void
807
973
  onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
974
+ viewerSceneSlot?: ReactNode
975
+ floorplanSceneSlot?: ReactNode
808
976
  }) {
809
977
  const viewMode = useEditor((s) => s.viewMode)
810
978
  const floorplanPaneRatio = useEditor((s) => s.floorplanPaneRatio)
@@ -816,6 +984,13 @@ const ViewerCanvas = memo(function ViewerCanvas({
816
984
  )
817
985
 
818
986
  const viewerAreaRef = useRef<HTMLDivElement>(null)
987
+ // State mirror of `viewerAreaRef` so the floorplan compass portal re-renders
988
+ // once the container exists (a plain ref mutation wouldn't trigger it).
989
+ const [viewerAreaEl, setViewerAreaEl] = useState<HTMLDivElement | null>(null)
990
+ const setViewerAreaNode = useCallback((el: HTMLDivElement | null) => {
991
+ viewerAreaRef.current = el
992
+ setViewerAreaEl(el)
993
+ }, [])
819
994
  const viewer3dRef = useRef<HTMLDivElement>(null)
820
995
  const isResizingFloorplan = useRef(false)
821
996
 
@@ -861,7 +1036,11 @@ const ViewerCanvas = memo(function ViewerCanvas({
861
1036
 
862
1037
  return (
863
1038
  <ErrorBoundary fallback={<EditorSceneCrashFallback />}>
864
- <div className="flex h-full" ref={viewerAreaRef}>
1039
+ {/* `relative` so the floorplan compass (portaled here to stay visible in
1040
+ 2d / 3d / split alike) can anchor to this container's bottom-left. */}
1041
+ <div className="relative flex h-full" ref={setViewerAreaNode}>
1042
+ <QuickMeasurementHud />
1043
+ <DeleteConfirmationDialog />
865
1044
  {/* 2D floorplan — always mounted once shown, hidden via CSS to preserve state */}
866
1045
  <div
867
1046
  className="relative h-full flex-shrink-0"
@@ -871,7 +1050,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
871
1050
  }}
872
1051
  >
873
1052
  <div className="h-full w-full overflow-hidden">
874
- <FloorplanPanel />
1053
+ <FloorplanPanel compassHost={viewerAreaEl} floorplanSceneSlot={floorplanSceneSlot} />
875
1054
  </div>
876
1055
  {viewMode === 'split' && (
877
1056
  <div
@@ -886,6 +1065,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
886
1065
  {/* 3D viewer — always mounted, hidden via CSS to avoid destroying the WebGL context */}
887
1066
  <div
888
1067
  className="relative min-w-0 flex-1 overflow-hidden"
1068
+ data-pascal-viewer-3d
889
1069
  ref={viewer3dRef}
890
1070
  style={{ display: show3d ? undefined : 'none' }}
891
1071
  >
@@ -918,6 +1098,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
918
1098
  isStudioMode={isStudioMode}
919
1099
  isVersionPreviewMode={isVersionPreviewMode}
920
1100
  onThumbnailCapture={onThumbnailCapture}
1101
+ viewerSceneSlot={viewerSceneSlot}
921
1102
  />
922
1103
  </Viewer>
923
1104
  </div>
@@ -935,7 +1116,11 @@ export default function Editor({
935
1116
  sidebarTabs,
936
1117
  viewerToolbarLeft,
937
1118
  viewerToolbarRight,
1119
+ stageOverlay,
938
1120
  inspectorFooter,
1121
+ multiSelectionFooter,
1122
+ viewerSceneSlot,
1123
+ floorplanSceneSlot,
939
1124
  projectId,
940
1125
  onLoad,
941
1126
  onSave,
@@ -944,6 +1129,7 @@ export default function Editor({
944
1129
  previewScene,
945
1130
  isVersionPreviewMode = false,
946
1131
  isLoading = false,
1132
+ onLoaderChange,
947
1133
  onThumbnailCapture,
948
1134
  sidebarOverlay,
949
1135
  viewerBanner,
@@ -974,16 +1160,29 @@ export default function Editor({
974
1160
  const sidebarWidth = useSidebarStore((s) => s.width)
975
1161
  const isSidebarCollapsed = useSidebarStore((s) => s.isCollapsed)
976
1162
 
1163
+ // Host-registered rail panels. Called unconditionally so
1164
+ // hook order is stable across the v1 / v2 layout branches below; the v1
1165
+ // AppSidebar path merges its own copy internally, the v2 path merges these
1166
+ // into its tab bar.
1167
+ const hostRailPanels = useHostPanels()
1168
+
977
1169
  useEffect(() => {
978
1170
  const teardown = initializeEditorRuntime()
979
1171
  return teardown
980
1172
  }, [])
981
1173
 
1174
+ useEffect(() => {
1175
+ void useEditor.persist.rehydrate()
1176
+ void useSidebarStore.persist.rehydrate()
1177
+ }, [])
1178
+
982
1179
  useEffect(() => {
983
1180
  useViewer.getState().setProjectId(projectId ?? null)
1181
+ useFloorplanMode.getState().setProjectId(projectId ?? null)
984
1182
 
985
1183
  return () => {
986
1184
  useViewer.getState().setProjectId(null)
1185
+ useFloorplanMode.getState().setProjectId(null)
987
1186
  }
988
1187
  }, [projectId])
989
1188
 
@@ -1039,13 +1238,10 @@ export default function Editor({
1039
1238
 
1040
1239
  // Lock scene graph and reset to select mode when entering version preview
1041
1240
  useEffect(() => {
1042
- useScene.getState().setReadOnly(isVersionPreviewMode)
1043
- if (isVersionPreviewMode) {
1044
- useEditor.getState().setMode('select')
1045
- }
1046
- return () => {
1047
- useScene.getState().setReadOnly(false)
1048
- }
1241
+ if (!isVersionPreviewMode) return
1242
+ const releaseReadOnly = acquireSceneReadOnlyLease()
1243
+ useEditor.getState().setMode('select')
1244
+ return releaseReadOnly
1049
1245
  }, [isVersionPreviewMode])
1050
1246
 
1051
1247
  useEffect(() => {
@@ -1059,8 +1255,25 @@ export default function Editor({
1059
1255
  setIsViewerSceneReady(ready)
1060
1256
  }, [])
1061
1257
 
1258
+ useEffect(() => {
1259
+ if (isLoading || isSceneLoading || !hasLoadedInitialScene || isViewerSceneReady) return
1260
+
1261
+ const timer = window.setTimeout(() => {
1262
+ console.warn('[editor] viewer scene readiness timed out; showing editor shell anyway', {
1263
+ sceneReadyKey,
1264
+ })
1265
+ setIsViewerSceneReady(true)
1266
+ }, SCENE_READY_FALLBACK_MS)
1267
+
1268
+ return () => window.clearTimeout(timer)
1269
+ }, [hasLoadedInitialScene, isLoading, isSceneLoading, isViewerSceneReady, sceneReadyKey])
1270
+
1062
1271
  const showLoader = isLoading || isSceneLoading || !hasLoadedInitialScene || !isViewerSceneReady
1063
1272
 
1273
+ useEffect(() => {
1274
+ onLoaderChange?.(showLoader)
1275
+ }, [showLoader, onLoaderChange])
1276
+
1064
1277
  const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
1065
1278
  const wasFirstPersonModeRef = useRef(isFirstPersonMode)
1066
1279
 
@@ -1109,6 +1322,7 @@ export default function Editor({
1109
1322
  <CeilingSystem />
1110
1323
  <RoofEditSystem />
1111
1324
  <StairEditSystem />
1325
+ {isFirstPersonMode && <FirstPersonControls />}
1112
1326
  <CustomCameraControls />
1113
1327
  <ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
1114
1328
  <InteractiveSystem />
@@ -1126,12 +1340,23 @@ export default function Editor({
1126
1340
  onThumbnailCapture={onThumbnailCapture}
1127
1341
  sceneReadyKey={sceneReadyKey}
1128
1342
  showLoader={showLoader}
1343
+ viewerSceneSlot={viewerSceneSlot}
1344
+ floorplanSceneSlot={floorplanSceneSlot}
1129
1345
  />
1130
1346
  )
1131
1347
 
1132
1348
  // ── V2 layout ──
1133
1349
  if (layoutVersion === 'v2') {
1134
- const tabMap = new Map(sidebarTabs?.map((t) => [t.id, t]) ?? [])
1350
+ // Registered host panels join the host's `sidebarTabs` as first-class tabs.
1351
+ // Explicit tabs keep precedence because they are already in the map first.
1352
+ const tabMap = new Map<string, SidebarTab & { component: React.ComponentType }>(
1353
+ sidebarTabs?.map((t) => [t.id, t]) ?? [],
1354
+ )
1355
+ for (const p of hostRailPanels) {
1356
+ if (!tabMap.has(p.id)) {
1357
+ tabMap.set(p.id, { id: p.id, label: p.label, icon: p.icon, component: p.component })
1358
+ }
1359
+ }
1135
1360
 
1136
1361
  const renderTabContent = (tabId: string) => {
1137
1362
  // Built-in panels
@@ -1148,17 +1373,28 @@ export default function Editor({
1148
1373
  return <Component />
1149
1374
  }
1150
1375
 
1151
- const tabBarTabs =
1152
- sidebarTabs?.map(({ id, label, mobileDefaultSnap, mobileIcon, icon }) => ({
1376
+ const tabBarTabs = [
1377
+ ...(sidebarTabs?.map(({ id, label, mobileDefaultSnap, mobileIcon, icon }) => ({
1153
1378
  id,
1154
1379
  label,
1155
1380
  mobileDefaultSnap,
1156
1381
  mobileIcon,
1157
1382
  icon,
1158
- })) ?? []
1383
+ })) ?? []),
1384
+ // Host panels appear after the explicit tabs in the rail. The icon
1385
+ // doubles as the mobile icon; a half-height sheet is a sensible default.
1386
+ ...hostRailPanels.map((p) => ({
1387
+ id: p.id,
1388
+ label: p.label,
1389
+ mobileDefaultSnap: 0.5,
1390
+ mobileIcon: p.icon,
1391
+ icon: p.icon,
1392
+ })),
1393
+ ]
1159
1394
 
1160
1395
  return (
1161
1396
  <>
1397
+ <FloorplanModeCoordinator />
1162
1398
  {showLoader && (
1163
1399
  <div className="fixed inset-0 z-60">
1164
1400
  <SceneLoader className="bg-background" />
@@ -1167,8 +1403,14 @@ export default function Editor({
1167
1403
 
1168
1404
  {!isLoading && isPreviewMode ? (
1169
1405
  <div className="dark flex h-full w-full flex-col bg-neutral-100 text-foreground">
1170
- <ViewerOverlay onBack={() => useEditor.getState().setPreviewMode(false)} />
1171
- <div className="h-full w-full">{previewViewerContent}</div>
1406
+ {isFirstPersonMode ? (
1407
+ <FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
1408
+ ) : (
1409
+ <ViewerOverlay onBack={() => useEditor.getState().setPreviewMode(false)} />
1410
+ )}
1411
+ <div className="h-full w-full" data-pascal-viewer-3d>
1412
+ {previewViewerContent}
1413
+ </div>
1172
1414
  </div>
1173
1415
  ) : (
1174
1416
  <>
@@ -1176,7 +1418,7 @@ export default function Editor({
1176
1418
  navbarSlot={navbarSlot}
1177
1419
  overlays={
1178
1420
  <>
1179
- {!isCaptureMode && <FloatingLevelSelector />}
1421
+ {!(isCaptureMode || stageOverlay) && <FloatingLevelSelector />}
1180
1422
  {!(isVersionPreviewMode || isCaptureMode || isStudioMode) && (
1181
1423
  <div className="pointer-events-auto">
1182
1424
  <ActionMenu />
@@ -1184,7 +1426,10 @@ export default function Editor({
1184
1426
  )}
1185
1427
  {!(isVersionPreviewMode || isCaptureMode || isStudioMode) && (
1186
1428
  <div className="pointer-events-auto">
1187
- <PanelManager inspectorFooter={inspectorFooter} />
1429
+ <PanelManager
1430
+ inspectorFooter={inspectorFooter}
1431
+ multiSelectionFooter={multiSelectionFooter}
1432
+ />
1188
1433
  </div>
1189
1434
  )}
1190
1435
  {!isCaptureMode && (
@@ -1204,6 +1449,7 @@ export default function Editor({
1204
1449
  renderTabContent={renderTabContent}
1205
1450
  sidebarOverlay={sidebarOverlay}
1206
1451
  sidebarTabs={tabBarTabs}
1452
+ stageOverlay={stageOverlay}
1207
1453
  viewerContent={viewerCanvas}
1208
1454
  viewerToolbarLeft={viewerToolbarLeft}
1209
1455
  viewerToolbarRight={viewerToolbarRight}
@@ -1224,6 +1470,7 @@ export default function Editor({
1224
1470
 
1225
1471
  return (
1226
1472
  <div className="dark flex h-full w-full gap-3 bg-neutral-100 p-3 text-foreground">
1473
+ <FloorplanModeCoordinator />
1227
1474
  {showLoader && (
1228
1475
  <div className="fixed inset-0 z-60">
1229
1476
  <SceneLoader className="bg-background" />
@@ -1232,8 +1479,14 @@ export default function Editor({
1232
1479
 
1233
1480
  {!isLoading && isPreviewMode ? (
1234
1481
  <>
1235
- <ViewerOverlay onBack={() => useEditor.getState().setPreviewMode(false)} />
1236
- <div className="h-full w-full">{previewViewerContent}</div>
1482
+ {isFirstPersonMode ? (
1483
+ <FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
1484
+ ) : (
1485
+ <ViewerOverlay onBack={() => useEditor.getState().setPreviewMode(false)} />
1486
+ )}
1487
+ <div className="h-full w-full" data-pascal-viewer-3d>
1488
+ {previewViewerContent}
1489
+ </div>
1237
1490
  </>
1238
1491
  ) : (
1239
1492
  <>
@@ -1263,6 +1516,7 @@ export default function Editor({
1263
1516
  <div className="pointer-events-auto">
1264
1517
  <HelperManager />
1265
1518
  </div>
1519
+ <RiserDiagramPanel />
1266
1520
  {isFirstPersonMode && (
1267
1521
  <FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
1268
1522
  )}