@pascal-app/editor 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/README.md +106 -0
  2. package/package.json +39 -37
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/bake-thumbnail.tsx +6 -15
  5. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  6. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  7. package/src/components/editor/custom-camera-controls.tsx +81 -8
  8. package/src/components/editor/editor-layout-mobile.tsx +1 -1
  9. package/src/components/editor/editor-layout-v2.tsx +1 -1
  10. package/src/components/editor/export-manager.tsx +14 -19
  11. package/src/components/editor/first-person/build-collider-world.test.ts +269 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +22 -5
  13. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  14. package/src/components/editor/first-person/ground-holes.ts +130 -0
  15. package/src/components/editor/first-person/mechanism-targets.test.ts +89 -2
  16. package/src/components/editor/first-person/mechanism-targets.ts +52 -0
  17. package/src/components/editor/first-person-controls.tsx +19 -11
  18. package/src/components/editor/floor-aware-camera-controls.test.ts +24 -0
  19. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  20. package/src/components/editor/floorplan-panel.tsx +72 -23
  21. package/src/components/editor/follow-pascal.test.tsx +912 -0
  22. package/src/components/editor/follow-pascal.tsx +354 -0
  23. package/src/components/editor/group-actions.ts +2 -0
  24. package/src/components/editor/group-move-3d.ts +2 -0
  25. package/src/components/editor/group-rotate-handle.tsx +2 -0
  26. package/src/components/editor/group-transform-shared.test.ts +460 -0
  27. package/src/components/editor/index.tsx +166 -79
  28. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  29. package/src/components/editor/room-highlight.tsx +13 -5
  30. package/src/components/editor/room-labels.test.ts +621 -128
  31. package/src/components/editor/room-labels.tsx +1090 -190
  32. package/src/components/editor/selection-manager.tsx +38 -117
  33. package/src/components/editor/slab-hole-highlights.tsx +2 -2
  34. package/src/components/editor/snapshot-capture-overlay.tsx +3 -2
  35. package/src/components/editor/snapshot-capture.test.ts +69 -58
  36. package/src/components/editor/snapshot-capture.ts +7 -1
  37. package/src/components/editor/thumbnail-generator.test.ts +350 -4
  38. package/src/components/editor/thumbnail-generator.tsx +128 -54
  39. package/src/components/editor/use-floorplan-background-placement.ts +3 -1
  40. package/src/components/editor/views/plugins-view.tsx +1 -8
  41. package/src/components/editor/views/view-bar.test.tsx +54 -0
  42. package/src/components/editor/views/view-bar.tsx +140 -17
  43. package/src/components/editor/wall-snap-beacon-layer.tsx +5 -4
  44. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  45. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  46. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +3 -4
  47. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +1 -1
  48. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +29 -7
  49. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  50. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  51. package/src/components/point-ask/bubble.tsx +538 -0
  52. package/src/components/point-ask/hover-chip.tsx +91 -0
  53. package/src/components/point-ask/mode-pill.tsx +92 -0
  54. package/src/components/point-ask/optional-motion.ts +22 -0
  55. package/src/components/point-ask/outline-corners.tsx +93 -0
  56. package/src/components/point-ask/pins.tsx +711 -0
  57. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  58. package/src/components/point-ask/point-ask-styles.ts +105 -0
  59. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  60. package/src/components/point-ask/region-marquee.tsx +64 -0
  61. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  62. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  63. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  64. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  65. package/src/components/tools/distribution/distribution-public.ts +9 -0
  66. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  67. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  68. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  69. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  70. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  71. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  72. package/src/components/tools/distribution/ports.test.ts +56 -0
  73. package/src/components/tools/distribution/ports.ts +378 -0
  74. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  75. package/src/components/tools/distribution/run-cursor.ts +36 -0
  76. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  77. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  78. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  79. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  80. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  81. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  82. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  83. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  84. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  85. package/src/components/tools/item/placement-history.test.tsx +38 -0
  86. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  87. package/src/components/tools/item/placement-strategies.ts +3 -6
  88. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  89. package/src/components/tools/item/use-draft-node.test.tsx +94 -94
  90. package/src/components/tools/item/use-placement-coordinator.tsx +55 -49
  91. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  92. package/src/components/tools/registry/item-surface-move.ts +2 -0
  93. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  94. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +63 -29
  96. package/src/components/tools/tool-manager.tsx +14 -5
  97. package/src/components/tools/wall/wall-drafting.test.ts +170 -57
  98. package/src/components/ui/action-menu/action-button.tsx +6 -1
  99. package/src/components/ui/action-menu/camera-actions.tsx +7 -3
  100. package/src/components/ui/action-menu/control-modes.tsx +13 -7
  101. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  102. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  103. package/src/components/ui/action-menu/index.tsx +56 -33
  104. package/src/components/ui/action-menu/measurement-control.tsx +11 -9
  105. package/src/components/ui/action-menu/placement.ts +20 -0
  106. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  107. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  108. package/src/components/ui/build-panel/build-panel.tsx +10 -3
  109. package/src/components/ui/command-palette/editor-commands.tsx +3 -12
  110. package/src/components/ui/command-palette/index.tsx +45 -15
  111. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  112. package/src/components/ui/controls/action-button.tsx +5 -2
  113. package/src/components/ui/controls/metric-control.tsx +1 -1
  114. package/src/components/ui/controls/segmented-control.tsx +21 -8
  115. package/src/components/ui/controls/select-control.tsx +1 -1
  116. package/src/components/ui/controls/slider-control.tsx +1 -1
  117. package/src/components/ui/controls/toggle-control.tsx +3 -3
  118. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  119. package/src/components/ui/floating-level-selector.tsx +150 -45
  120. package/src/components/ui/helpers/contextual-helper-panel.tsx +32 -7
  121. package/src/components/ui/helpers/helper-manager.test.tsx +54 -2
  122. package/src/components/ui/helpers/helper-manager.tsx +7 -1
  123. package/src/components/ui/helpers/item-helper.tsx +9 -1
  124. package/src/components/ui/panels/collections/collections-popover.tsx +8 -8
  125. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  126. package/src/components/ui/panels/mobile-selection-bar.tsx +6 -3
  127. package/src/components/ui/panels/multi-selection-panel.tsx +120 -3
  128. package/src/components/ui/panels/panel-wrapper.tsx +53 -27
  129. package/src/components/ui/panels/parametric-field-control.tsx +2 -2
  130. package/src/components/ui/panels/parametric-inspector.tsx +3 -3
  131. package/src/components/ui/panels/room-panel.tsx +65 -0
  132. package/src/components/ui/primitives/badge.tsx +8 -0
  133. package/src/components/ui/primitives/sidebar.tsx +1 -1
  134. package/src/components/ui/right-stack.tsx +21 -2
  135. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  136. package/src/components/ui/sidebar/icon-rail.tsx +3 -3
  137. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  138. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  139. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  140. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  141. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  142. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  143. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +107 -23
  144. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  145. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  146. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  147. package/src/components/ui/sidebar/panels/site-panel/index.tsx +5 -12
  148. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -10
  149. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -2
  150. package/src/components/ui/sidebar/tab-bar.tsx +4 -4
  151. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  152. package/src/components/viewer/viewer-rooms.tsx +16 -0
  153. package/src/hooks/use-auto-save.integration.test.tsx +96 -38
  154. package/src/hooks/use-auto-save.ts +26 -1
  155. package/src/hooks/use-ceiling-events.test.ts +123 -118
  156. package/src/hooks/use-grid-events.test.ts +105 -105
  157. package/src/hooks/use-keyboard.test.ts +65 -16
  158. package/src/hooks/use-keyboard.ts +29 -12
  159. package/src/hooks/use-placement-notice.test.ts +24 -1
  160. package/src/hooks/use-placement-notice.ts +37 -6
  161. package/src/index.tsx +71 -24
  162. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  163. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  164. package/src/lib/add-storey.test.ts +177 -0
  165. package/src/lib/add-storey.ts +59 -0
  166. package/src/lib/below-grade.test.tsx +171 -0
  167. package/src/lib/below-grade.ts +133 -0
  168. package/src/lib/camera-facing.test.ts +24 -0
  169. package/src/lib/camera-facing.ts +24 -0
  170. package/src/lib/direct-manipulation.ts +2 -0
  171. package/src/lib/download-project-file.ts +17 -0
  172. package/src/lib/editor-level-display.ts +9 -2
  173. package/src/lib/editor-views.ts +35 -1
  174. package/src/lib/export-texture-utils.ts +2 -4
  175. package/src/lib/floor-foundation-support.test.ts +1 -0
  176. package/src/lib/floorplan/floorplan-pdfkit-document.ts +31 -16
  177. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +39 -0
  178. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +4 -0
  179. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  180. package/src/lib/floorplan/sheet-export.test.ts +24 -0
  181. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +34 -45
  182. package/src/lib/floorplan/site-plan/coverage.ts +2 -4
  183. package/src/lib/floorplan/site-plan/front-edge.ts +2 -1
  184. package/src/lib/floorplan/site-plan/geometry.test.ts +9 -15
  185. package/src/lib/floorplan/site-plan/geometry.ts +1 -416
  186. package/src/lib/floorplan/site-plan/index.ts +0 -16
  187. package/src/lib/floorplan/site-plan/site-annotations.test.ts +117 -0
  188. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -2
  189. package/src/lib/floorplan/site-plan/site-parts.ts +14 -106
  190. package/src/lib/floorplan/site-plan/yards.test.ts +2 -1
  191. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  192. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  193. package/src/lib/follow-pascal-exterior.ts +39 -0
  194. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  195. package/src/lib/follow-pascal-presentation.ts +129 -0
  196. package/src/lib/follow-pascal-sight.ts +84 -0
  197. package/src/lib/follow-pascal-targets.test.ts +406 -0
  198. package/src/lib/follow-pascal-targets.ts +198 -0
  199. package/src/lib/follow-pascal.test.ts +350 -0
  200. package/src/lib/follow-pascal.ts +284 -0
  201. package/src/lib/fresh-planar-placement.test.ts +112 -0
  202. package/src/lib/fresh-planar-placement.ts +57 -3
  203. package/src/lib/geometry-script/author.ts +6 -1
  204. package/src/lib/geometry-script/client.ts +8 -1
  205. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  206. package/src/lib/geometry-script/protocol.ts +1 -0
  207. package/src/lib/glb-export.test.ts +187 -162
  208. package/src/lib/glb-export.ts +4 -1
  209. package/src/lib/history.test.ts +12 -7
  210. package/src/lib/interaction/scope.ts +4 -0
  211. package/src/lib/level-selection.test.ts +57 -0
  212. package/src/lib/level-selection.ts +16 -1
  213. package/src/lib/lot/clean-ring.test.ts +2 -1
  214. package/src/lib/lot/drop-in.ts +9 -24
  215. package/src/lib/lot/lot-patch.test.ts +1 -1
  216. package/src/lib/lot/lot-patch.ts +2 -2
  217. package/src/lib/measurement-kind.test.ts +6 -8
  218. package/src/lib/measurements.test.ts +23 -0
  219. package/src/lib/orbit-floor-clearance.test.ts +36 -0
  220. package/src/lib/orbit-floor-clearance.ts +48 -0
  221. package/src/lib/paint-pass-through.test.ts +11 -0
  222. package/src/lib/paint-pass-through.ts +4 -2
  223. package/src/lib/paint-scope.ts +1 -0
  224. package/src/lib/parametric-node-update.ts +16 -7
  225. package/src/lib/plugin-chat.ts +15 -0
  226. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  227. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  228. package/src/lib/point-ask/ask-created.test.ts +195 -0
  229. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  230. package/src/lib/point-ask/build-context.test.ts +173 -0
  231. package/src/lib/point-ask/build-context.ts +94 -0
  232. package/src/lib/point-ask/capture-crop.ts +129 -0
  233. package/src/lib/point-ask/choreography.ts +69 -0
  234. package/src/lib/point-ask/compare.test.ts +145 -0
  235. package/src/lib/point-ask/compare.ts +73 -0
  236. package/src/lib/point-ask/describe-target.test.ts +165 -0
  237. package/src/lib/point-ask/describe-target.ts +234 -0
  238. package/src/lib/point-ask/dictation.ts +79 -0
  239. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  240. package/src/lib/point-ask/edge-marker.ts +27 -0
  241. package/src/lib/point-ask/frame-loop.ts +25 -0
  242. package/src/lib/point-ask/history-marks.test.ts +94 -0
  243. package/src/lib/point-ask/history-marks.ts +39 -0
  244. package/src/lib/point-ask/host-api.ts +32 -0
  245. package/src/lib/point-ask/outline-box.test.ts +30 -0
  246. package/src/lib/point-ask/outline-box.ts +11 -0
  247. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  248. package/src/lib/point-ask/pick.test.ts +220 -0
  249. package/src/lib/point-ask/pick.ts +121 -0
  250. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  251. package/src/lib/point-ask/pin-machine.ts +217 -0
  252. package/src/lib/point-ask/placement.test.ts +203 -0
  253. package/src/lib/point-ask/placement.ts +141 -0
  254. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  255. package/src/lib/point-ask/projector.ts +83 -0
  256. package/src/lib/point-ask/region.test.ts +157 -0
  257. package/src/lib/point-ask/region.ts +136 -0
  258. package/src/lib/point-ask/retry.test.ts +217 -0
  259. package/src/lib/point-ask/retry.ts +37 -0
  260. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  261. package/src/lib/point-ask/scene-commit.ts +32 -0
  262. package/src/lib/point-ask/session.ts +1391 -0
  263. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  264. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  265. package/src/lib/point-ask/spring.test.ts +63 -0
  266. package/src/lib/point-ask/spring.ts +107 -0
  267. package/src/lib/point-ask/suggestions.test.ts +80 -0
  268. package/src/lib/point-ask/suggestions.ts +125 -0
  269. package/src/lib/point-ask/target-scope.test.ts +622 -0
  270. package/src/lib/point-ask/target-scope.ts +338 -0
  271. package/src/lib/point-ask/types.ts +48 -0
  272. package/src/lib/portable-export.test.ts +34 -1
  273. package/src/lib/portable-export.ts +119 -53
  274. package/src/lib/print-feature-thickness.ts +10 -1
  275. package/src/lib/print-roof-solids.test.ts +39 -1
  276. package/src/lib/print-roof-solids.ts +43 -10
  277. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  278. package/src/lib/room-assembly-overlay.ts +2 -3
  279. package/src/lib/room-selection.test.ts +45 -1
  280. package/src/lib/scene-clipboard.test.ts +246 -1
  281. package/src/lib/scene-clipboard.ts +44 -14
  282. package/src/lib/selection-routing.ts +1 -1
  283. package/src/lib/sfx-bus.ts +13 -0
  284. package/src/lib/sfx-player.test.ts +70 -9
  285. package/src/lib/sfx-player.ts +6 -0
  286. package/src/lib/stair-levels.test.ts +25 -0
  287. package/src/lib/stair-levels.ts +22 -3
  288. package/src/lib/terrace-draft.ts +2 -1
  289. package/src/lib/viewer-selection.ts +22 -11
  290. package/src/lib/wall-placement-pointer.test.ts +42 -0
  291. package/src/lib/wall-placement-pointer.ts +57 -0
  292. package/src/lib/window-changes.ts +35 -0
  293. package/src/store/use-audio.test.ts +146 -0
  294. package/src/store/use-audio.tsx +127 -3
  295. package/src/store/use-construction-reveal.test.ts +173 -0
  296. package/src/store/use-construction-reveal.ts +145 -0
  297. package/src/store/use-corner-snap-hint.test.ts +24 -0
  298. package/src/store/use-corner-snap-hint.ts +47 -0
  299. package/src/store/use-follow-pascal.test.ts +62 -0
  300. package/src/store/use-follow-pascal.ts +53 -0
  301. package/src/store/use-point-ask.ts +247 -0
  302. package/src/store/use-stair-build-preview.ts +25 -5
  303. package/src/store/view-layout.test.ts +27 -1
  304. package/src/store/view-layout.ts +4 -1
  305. package/src/test-utils/render-current-markup.tsx +16 -0
  306. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  307. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  308. package/src/components/tools/stair/stair-defaults.ts +0 -15
  309. package/src/components/tools/stair/stair-tool.tsx +0 -682
@@ -19,6 +19,7 @@ import {
19
19
  SceneEnvironment,
20
20
  useViewer,
21
21
  Viewer,
22
+ type ViewerBreadcrumbHandler,
22
23
  type ViewerImmersiveSession,
23
24
  ViewerPresentations,
24
25
  } from '@pascal-app/viewer'
@@ -38,11 +39,14 @@ import { ViewerOverlay } from '../../components/viewer-overlay'
38
39
  import { ViewerZoneSystem } from '../../components/viewer-zone-system'
39
40
  import { type SaveStatus, useAutoSave } from '../../hooks/use-auto-save'
40
41
  import { useKeyboard } from '../../hooks/use-keyboard'
42
+ import { useIsMobile } from '../../hooks/use-mobile'
41
43
  import { useSaveShortcut } from '../../hooks/use-save-shortcut'
42
44
  import { useCeilingEditSessionOwner } from '../../lib/ceiling-edit-session'
43
45
  import { showsWholeBuilding, useEditorLevelDisplay } from '../../lib/editor-level-display'
44
46
  import { isSceneView } from '../../lib/editor-views'
47
+ import { initFinishScriptRefresh } from '../../lib/geometry-script/finish-scripts'
45
48
  import { useGestureLifecycleOwner } from '../../lib/gesture-lifecycle'
49
+ import { shouldShowEditingControls } from '../../lib/interaction/overlay-policy'
46
50
  import {
47
51
  createLocalProjectPresentationPersistence,
48
52
  type LocalProjectPresentationPersistence,
@@ -58,12 +62,14 @@ import {
58
62
  } from '../../lib/scene'
59
63
  import { disposeSFXBus, initSFXBus } from '../../lib/sfx-bus'
60
64
  import { useUnitFocusRules } from '../../lib/units'
65
+ import { cn } from '../../lib/utils'
61
66
  import { useViewerFloorDisplay } from '../../lib/viewer-selection'
62
67
  import { type CameraHintAction, useCameraHintFocus } from '../../store/use-camera-hint-focus'
63
68
  import useEditor from '../../store/use-editor'
64
69
  import useFloorplanMode from '../../store/use-floorplan-mode'
65
70
  import useSessionGroups from '../../store/use-session-groups'
66
71
  import { paneOfView, VIEW_2D, VIEW_3D, type ViewPaneIndex } from '../../store/view-layout'
72
+ import { PointAskLayer } from '../point-ask/point-ask-layer'
67
73
  import { CeilingSelectionAffordanceSystem } from '../systems/ceiling/ceiling-selection-affordance-system'
68
74
  import { CeilingSystem } from '../systems/ceiling/ceiling-system'
69
75
  import { RoofEditSystem } from '../systems/roof/roof-edit-system'
@@ -73,6 +79,7 @@ import { ZoneSystem } from '../systems/zone/zone-system'
73
79
  import { BoxSelectTool } from '../tools/select/box-select-tool'
74
80
  import { ToolManager } from '../tools/tool-manager'
75
81
  import { ActionMenu } from '../ui/action-menu'
82
+ import { type ActionMenuPlacement, TOP_MENU_CLEARANCE } from '../ui/action-menu/placement'
76
83
  import { CommandPalette, type CommandPaletteEmptyAction } from '../ui/command-palette'
77
84
  import { EditorCommands } from '../ui/command-palette/editor-commands'
78
85
  import { FloatingLevelSelector } from '../ui/floating-level-selector'
@@ -105,6 +112,7 @@ import { FloatingActionMenu } from './floating-action-menu'
105
112
  import { FloatingBuildingActionMenu } from './floating-building-action-menu'
106
113
  import { FloorplanModeCoordinator } from './floorplan-mode-coordinator'
107
114
  import { FloorplanPanel } from './floorplan-panel'
115
+ import { FollowPascal } from './follow-pascal'
108
116
  import { FootprintHeightHandle } from './footprint-height-handle'
109
117
  import { Grid } from './grid'
110
118
  import { GroupFloatingActionMenu } from './group-floating-action-menu'
@@ -156,6 +164,8 @@ const EDITOR_HOVER_STYLES: HoverStyles = {
156
164
  'paint-ready': { visibleColor: 0xf5_9e_0b, hiddenColor: 0xfd_e0_68, strength: 5, pulse: true },
157
165
  // Erasing paints nothing: a neutral outline, no paint colour.
158
166
  'erase-ready': { visibleColor: 0xe4_e4_e7, hiddenColor: 0xa1_a1_aa, strength: 5, pulse: false },
167
+ // Point and ask: the agent accent, steady (nothing animates a hover), the occluded parts faint.
168
+ point: { visibleColor: 0xa9_9b_ff, hiddenColor: 0x5b_4f_c9, strength: 5, pulse: false },
159
169
  'paint-disabled': {
160
170
  visibleColor: 0x94_a3_b8,
161
171
  hiddenColor: 0x47_55_69,
@@ -175,11 +185,13 @@ const EDITOR_DEFAULT_RENDER = { shading: 'solid' } as const
175
185
  function initializeEditorRuntime(): () => void {
176
186
  const unsubscribeSpatialGrid = initSpatialGridSync()
177
187
  const unsubscribeSpaceDetection = initSpaceDetectionSync(useScene, useEditor)
188
+ const stopFinishScriptRefresh = initFinishScriptRefresh()
178
189
  initSFXBus()
179
190
 
180
191
  return () => {
181
192
  unsubscribeSpatialGrid()
182
193
  unsubscribeSpaceDetection?.()
194
+ stopFinishScriptRefresh()
183
195
 
184
196
  spatialGridManager.clear()
185
197
  disposeSFXBus()
@@ -192,6 +204,8 @@ function initializeEditorRuntime(): () => void {
192
204
  export interface EditorProps {
193
205
  // Layout version — 'v1' (default) or 'v2' (navbar + two-column)
194
206
  layoutVersion?: 'v1' | 'v2'
207
+ // Viewport edge the tool menu docks to (desktop only; mobile stays at the bottom)
208
+ actionMenuPlacement?: ActionMenuPlacement
195
209
 
196
210
  // UI slots (v1)
197
211
  appMenuButton?: ReactNode
@@ -257,13 +271,23 @@ export interface EditorProps {
257
271
  // Loading indicator (e.g. project fetching in community mode)
258
272
  isLoading?: boolean
259
273
 
260
- // Fires when the full-screen scene loader shows/hides — lets hosts measure
274
+ /**
275
+ * Replaces the loading surface inside the canvas panes in both layouts.
276
+ * Loading failures always use the editor's error/retry surface.
277
+ * Omit (or pass null) to use the default SceneLoader.
278
+ */
279
+ loadingSlot?: ReactNode
280
+
281
+ // Fires when scene loading starts/finishes — lets hosts measure
261
282
  // open-to-interactive time without reaching into internal loader state.
262
283
  onLoaderChange?: (visible: boolean) => void
263
284
 
264
285
  // Thumbnail
265
286
  onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
266
287
 
288
+ /** Viewer diagnostics for the host's error-reporter trail; see `<Viewer onBreadcrumb>`. */
289
+ onViewerBreadcrumb?: ViewerBreadcrumbHandler
290
+
267
291
  /**
268
292
  * When true, skip the viewer post-FX pipeline (same as `?disable=postFx`).
269
293
  * Hosts use this for a stable local "light preview" without relying only on
@@ -576,7 +600,13 @@ function CameraControlHintItem({ hint }: { hint: CameraControlHint }) {
576
600
  )
577
601
  }
578
602
 
579
- function ViewerCanvasControlsHint({ onDismiss }: { onDismiss: () => void }) {
603
+ function ViewerCanvasControlsHint({
604
+ belowTopMenu = false,
605
+ onDismiss,
606
+ }: {
607
+ belowTopMenu?: boolean
608
+ onDismiss: () => void
609
+ }) {
580
610
  const all = CAMERA_CONTROL_HINTS
581
611
  // A host teaching one gesture at a time narrows this to the one it is asking
582
612
  // for, and to nothing once it is done. Null — the default — is all of them.
@@ -588,7 +618,13 @@ function ViewerCanvasControlsHint({ onDismiss }: { onDismiss: () => void }) {
588
618
  }
589
619
 
590
620
  return (
591
- <div className="pointer-events-none absolute top-14 left-1/2 z-40 max-w-[calc(100%-2rem)] -translate-x-1/2">
621
+ <div
622
+ className={cn(
623
+ 'pointer-events-none absolute left-1/2 z-40 max-w-[calc(100%-2rem)] -translate-x-1/2',
624
+ !belowTopMenu && 'top-14',
625
+ )}
626
+ style={belowTopMenu ? { top: TOP_MENU_CLEARANCE } : undefined}
627
+ >
592
628
  <section
593
629
  aria-label="Camera controls hint"
594
630
  className="pointer-events-auto flex items-start gap-3 rounded-2xl border border-border/35 bg-background/90 px-3.5 py-2.5 shadow-elevation-4 backdrop-blur-xl"
@@ -877,6 +913,7 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
877
913
  {isFirstPersonMode && <FirstPersonControls />}
878
914
  {isCaptureMode && !isXRMode && <CaptureCameraRig />}
879
915
  {!isXRMode && <CustomCameraControls />}
916
+ {!isXRMode && <FollowPascal />}
880
917
  {!isXRMode && <ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />}
881
918
  {!isXRMode && <VectorEdgeExtractor />}
882
919
  {!(isFirstPersonMode || isXRMode) && <SiteEdgeLabels />}
@@ -1077,14 +1114,18 @@ const ViewerCanvas = memo(function ViewerCanvas({
1077
1114
  isStudioMode,
1078
1115
  hasLoadedInitialScene,
1079
1116
  showLoader,
1117
+ isSceneDataLoading,
1118
+ loadingOverlay,
1080
1119
  sceneReadyKey,
1081
1120
  onSceneReadyChange,
1082
1121
  onThumbnailCapture,
1122
+ onViewerBreadcrumb,
1083
1123
  presentationsReady,
1084
1124
  viewerSceneSlot,
1085
1125
  floorplanSceneSlot,
1086
1126
  disablePostFx = false,
1087
1127
  immersive,
1128
+ hintBelowTopMenu = false,
1088
1129
  }: {
1089
1130
  isVersionPreviewMode: boolean
1090
1131
  isLoading: boolean
@@ -1092,14 +1133,18 @@ const ViewerCanvas = memo(function ViewerCanvas({
1092
1133
  isStudioMode: boolean
1093
1134
  hasLoadedInitialScene: boolean
1094
1135
  showLoader: boolean
1136
+ isSceneDataLoading: boolean
1137
+ loadingOverlay: ReactNode
1095
1138
  sceneReadyKey: number
1096
1139
  onSceneReadyChange: (ready: boolean) => void
1097
1140
  onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
1141
+ onViewerBreadcrumb?: ViewerBreadcrumbHandler
1098
1142
  presentationsReady: boolean
1099
1143
  viewerSceneSlot?: ReactNode
1100
1144
  floorplanSceneSlot?: ReactNode
1101
1145
  disablePostFx?: boolean
1102
1146
  immersive?: ViewerImmersiveSession
1147
+ hintBelowTopMenu?: boolean
1103
1148
  }) {
1104
1149
  const layout = useActiveViewLayout()
1105
1150
  const views = useEditorViews()
@@ -1204,18 +1249,22 @@ const ViewerCanvas = memo(function ViewerCanvas({
1204
1249
  >
1205
1250
  {/* 2D floorplan — always mounted once shown, hidden via CSS to preserve state */}
1206
1251
  <div
1252
+ aria-busy={isSceneDataLoading}
1207
1253
  className="relative min-w-0 overflow-hidden"
1254
+ data-pascal-viewer-2d
1208
1255
  data-view-pane={pane2d ?? undefined}
1209
1256
  onPointerDownCapture={pane2d === null ? undefined : focusPane(pane2d)}
1210
1257
  style={paneStyle(pane2d)}
1211
1258
  >
1212
- <div className="h-full w-full overflow-hidden">
1259
+ <div className="relative isolate h-full w-full overflow-hidden">
1213
1260
  <FloorplanPanel compassHost={viewerAreaEl} floorplanSceneSlot={floorplanSceneSlot} />
1214
1261
  </div>
1262
+ {isSceneDataLoading && loadingOverlay}
1215
1263
  </div>
1216
1264
 
1217
1265
  {/* 3D viewer — always mounted, hidden via CSS to avoid destroying the WebGL context */}
1218
1266
  <div
1267
+ aria-busy={showLoader}
1219
1268
  className="relative min-w-0 overflow-hidden"
1220
1269
  data-pascal-viewer-3d
1221
1270
  data-view-pane={pane3d ?? undefined}
@@ -1223,47 +1272,55 @@ const ViewerCanvas = memo(function ViewerCanvas({
1223
1272
  ref={viewer3dRef}
1224
1273
  style={paneStyle(pane3d)}
1225
1274
  >
1226
- <DeleteCursorLayer
1227
- containerRef={viewer3dRef}
1228
- isVersionPreviewMode={isVersionPreviewMode}
1229
- />
1230
- <PaintCursorLayer
1231
- containerRef={viewer3dRef}
1232
- isVersionPreviewMode={isVersionPreviewMode}
1233
- />
1234
- {!showLoader && isCameraControlsHintVisible && !isFirstPersonMode ? (
1235
- <ViewerCanvasControlsHint onDismiss={dismissCameraControlsHint} />
1236
- ) : null}
1237
- <SelectionPersistenceManager enabled={hasLoadedInitialScene && !showLoader} />
1238
- <Viewer
1239
- defaultRender={EDITOR_DEFAULT_RENDER}
1240
- disablePostFx={disablePostFx}
1241
- hoverStyles={EDITOR_HOVER_STYLES}
1242
- isolate={presetIsolation}
1243
- // Preset captures isolate one subtree and keep the exterior transparent.
1244
- // Other modes retain the viewer's configured background policy.
1245
- transparent={presetIsolation === null ? undefined : true}
1246
- onSceneReadyChange={onSceneReadyChange}
1247
- renderContext="editor"
1248
- renderPaused={!show3d && !showLoader}
1249
- sceneReadyKey={sceneReadyKey}
1250
- immersive={immersive}
1251
- // Walk/drone framing during snapshot capture is camera-only: the
1252
- // viewer's default selection manager would hover-highlight whatever
1253
- // the cursor crosses, which orbit capture never does.
1254
- selectionManager={isFirstPersonMode && !isCaptureMode ? 'default' : 'custom'}
1255
- >
1256
- <ViewerSceneContent
1257
- isFirstPersonMode={isFirstPersonMode}
1258
- isLoading={showLoader}
1259
- isXRMode={immersive != null}
1260
- isStudioMode={isStudioMode}
1275
+ <div className="relative isolate h-full">
1276
+ <DeleteCursorLayer
1277
+ containerRef={viewer3dRef}
1278
+ isVersionPreviewMode={isVersionPreviewMode}
1279
+ />
1280
+ <PaintCursorLayer
1281
+ containerRef={viewer3dRef}
1261
1282
  isVersionPreviewMode={isVersionPreviewMode}
1262
- onThumbnailCapture={onThumbnailCapture}
1263
- presentationsReady={presentationsReady}
1264
- viewerSceneSlot={viewerSceneSlot}
1265
1283
  />
1266
- </Viewer>
1284
+ {!isVersionPreviewMode && <PointAskLayer />}
1285
+ {!showLoader && isCameraControlsHintVisible && !isFirstPersonMode ? (
1286
+ <ViewerCanvasControlsHint
1287
+ belowTopMenu={hintBelowTopMenu}
1288
+ onDismiss={dismissCameraControlsHint}
1289
+ />
1290
+ ) : null}
1291
+ <SelectionPersistenceManager enabled={hasLoadedInitialScene && !showLoader} />
1292
+ <Viewer
1293
+ defaultRender={EDITOR_DEFAULT_RENDER}
1294
+ disablePostFx={disablePostFx}
1295
+ hoverStyles={EDITOR_HOVER_STYLES}
1296
+ isolate={presetIsolation}
1297
+ // Preset captures isolate one subtree and keep the exterior transparent.
1298
+ // Other modes retain the viewer's configured background policy.
1299
+ transparent={presetIsolation === null ? undefined : true}
1300
+ onSceneReadyChange={onSceneReadyChange}
1301
+ onBreadcrumb={onViewerBreadcrumb}
1302
+ renderContext="editor"
1303
+ renderPaused={!show3d && !showLoader}
1304
+ sceneReadyKey={sceneReadyKey}
1305
+ immersive={immersive}
1306
+ // Walk/drone framing during snapshot capture is camera-only: the
1307
+ // viewer's default selection manager would hover-highlight whatever
1308
+ // the cursor crosses, which orbit capture never does.
1309
+ selectionManager={isFirstPersonMode && !isCaptureMode ? 'default' : 'custom'}
1310
+ >
1311
+ <ViewerSceneContent
1312
+ isFirstPersonMode={isFirstPersonMode}
1313
+ isLoading={showLoader}
1314
+ isXRMode={immersive != null}
1315
+ isStudioMode={isStudioMode}
1316
+ isVersionPreviewMode={isVersionPreviewMode}
1317
+ onThumbnailCapture={onThumbnailCapture}
1318
+ presentationsReady={presentationsReady}
1319
+ viewerSceneSlot={viewerSceneSlot}
1320
+ />
1321
+ </Viewer>
1322
+ </div>
1323
+ {showLoader && loadingOverlay}
1267
1324
  </div>
1268
1325
 
1269
1326
  {hostPanes.map((pane) => {
@@ -1273,6 +1330,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
1273
1330
  className="relative min-w-0 overflow-hidden"
1274
1331
  data-view-pane={pane}
1275
1332
  key={viewId}
1333
+ onFocusCapture={focusPane(pane)}
1276
1334
  onPointerDownCapture={focusPane(pane)}
1277
1335
  style={paneStyle(pane)}
1278
1336
  >
@@ -1302,6 +1360,7 @@ function PreviewStage({
1302
1360
  onModeChange,
1303
1361
  projectName,
1304
1362
  showLoader,
1363
+ loadingOverlay,
1305
1364
  viewerContent,
1306
1365
  }: {
1307
1366
  isFirstPersonMode: boolean
@@ -1309,6 +1368,7 @@ function PreviewStage({
1309
1368
  onModeChange: (mode: ViewerStageMode) => void
1310
1369
  projectName?: string | null
1311
1370
  showLoader: boolean
1371
+ loadingOverlay: ReactNode
1312
1372
  viewerContent: ReactNode
1313
1373
  }) {
1314
1374
  const hasFloorplan = useScene((state) =>
@@ -1350,6 +1410,7 @@ function PreviewStage({
1350
1410
  switcherClassName={`${PREVIEW_STAGE_SWITCHER_POSITION} ${showLoader ? 'z-[70]' : ''}`}
1351
1411
  >
1352
1412
  {viewerContent}
1413
+ {showLoader && loadingOverlay}
1353
1414
  </ViewerStage>
1354
1415
  </div>
1355
1416
  )
@@ -1358,6 +1419,7 @@ function PreviewStage({
1358
1419
  function EditorContent({
1359
1420
  guardAgainstSceneWipe,
1360
1421
  layoutVersion = 'v1',
1422
+ actionMenuPlacement,
1361
1423
  appMenuButton,
1362
1424
  sidebarTop,
1363
1425
  navbarSlot,
@@ -1380,8 +1442,10 @@ function EditorContent({
1380
1442
  previewScene,
1381
1443
  isVersionPreviewMode = false,
1382
1444
  isLoading = false,
1445
+ loadingSlot,
1383
1446
  onLoaderChange,
1384
1447
  onThumbnailCapture,
1448
+ onViewerBreadcrumb,
1385
1449
  disablePostFx = false,
1386
1450
  immersive,
1387
1451
  sidebarOverlay,
@@ -1392,6 +1456,9 @@ function EditorContent({
1392
1456
  extraSidebarPanels,
1393
1457
  commandPaletteEmptyAction,
1394
1458
  }: EditorProps) {
1459
+ const isMobile = useIsMobile()
1460
+ // A top-docked menu sits at the top of the scene panes on desktop (layout v2).
1461
+ const dockMenuOnTop = actionMenuPlacement === 'top' && !isMobile
1395
1462
  const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
1396
1463
  const isStudioMode = useEditor((s) => s.workspaceMode === 'studio')
1397
1464
  const presentationProjectId = projectId ?? null
@@ -1446,6 +1513,12 @@ function EditorContent({
1446
1513
  const [previewStageMode, setPreviewStageMode] = useState<ViewerStageMode>('3d')
1447
1514
  const isPreviewMode = useEditor((s) => s.isPreviewMode)
1448
1515
  const isCaptureMode = useEditor((s) => s.isCaptureMode)
1516
+ const readOnly = useScene((s) => s.readOnly)
1517
+ // Chrome only steps down for the top menu while the menu is actually there.
1518
+ const topMenuShown =
1519
+ dockMenuOnTop &&
1520
+ !(isVersionPreviewMode || isCaptureMode || isStudioMode) &&
1521
+ shouldShowEditingControls(readOnly)
1449
1522
 
1450
1523
  const sidebarWidth = useSidebarStore((s) => s.width)
1451
1524
  const isSidebarCollapsed = useSidebarStore((s) => s.isCollapsed)
@@ -1481,6 +1554,10 @@ function EditorContent({
1481
1554
  // Load on mount, project switches, and explicit retry attempts.
1482
1555
  useEffect(() => {
1483
1556
  let cancelled = false
1557
+ // The shell stays usable while loading; scene commands must not edit the
1558
+ // unloaded graph. Release synchronously before hydration so its normal
1559
+ // migrations and derived geometry can write, and retain the lock on error.
1560
+ let releaseLoadingLock = acquireSceneReadOnlyLease()
1484
1561
 
1485
1562
  async function load(attempt: number) {
1486
1563
  beginSceneLoad()
@@ -1497,6 +1574,7 @@ function EditorContent({
1497
1574
  try {
1498
1575
  const sceneGraph = onLoad ? await onLoad() : loadSceneFromLocalStorage()
1499
1576
  if (!cancelled && attempt === sceneLoadAttempt) {
1577
+ releaseLoadingLock()
1500
1578
  applySceneGraphToEditor(sceneGraph)
1501
1579
  // The store holds the loaded graph: autosave resumes now, not on a
1502
1580
  // frame — a tab loaded while hidden never gets one.
@@ -1509,6 +1587,8 @@ function EditorContent({
1509
1587
  // state: nothing may be written until a load actually succeeds.
1510
1588
  failed = true
1511
1589
  if (!cancelled) {
1590
+ releaseLoadingLock()
1591
+ releaseLoadingLock = acquireSceneReadOnlyLease()
1512
1592
  console.error('[editor] scene load failed', error)
1513
1593
  setSceneLoadError(error ?? new Error('Scene load failed'))
1514
1594
  }
@@ -1531,6 +1611,7 @@ function EditorContent({
1531
1611
 
1532
1612
  return () => {
1533
1613
  cancelled = true
1614
+ releaseLoadingLock()
1534
1615
  }
1535
1616
  }, [onLoad, beginSceneLoad, completeSceneLoad, sceneLoadAttempt])
1536
1617
 
@@ -1560,13 +1641,6 @@ function EditorContent({
1560
1641
  if (!isPreviewMode) setPreviewStageMode('3d')
1561
1642
  }, [isPreviewMode])
1562
1643
 
1563
- useEffect(() => {
1564
- document.body.classList.add('dark')
1565
- return () => {
1566
- document.body.classList.remove('dark')
1567
- }
1568
- }, [])
1569
-
1570
1644
  const handleSceneReadyChange = useCallback((ready: boolean) => {
1571
1645
  setIsViewerSceneReady(ready)
1572
1646
  }, [])
@@ -1584,7 +1658,8 @@ function EditorContent({
1584
1658
  return () => window.clearTimeout(timer)
1585
1659
  }, [hasLoadedInitialScene, isLoading, isSceneLoading, isViewerSceneReady, sceneReadyKey])
1586
1660
 
1587
- const showLoader = isLoading || isSceneLoading || !hasLoadedInitialScene || !isViewerSceneReady
1661
+ const isSceneDataLoading = isLoading || isSceneLoading || !hasLoadedInitialScene
1662
+ const showLoader = isSceneDataLoading || !isViewerSceneReady
1588
1663
  const visibleLoader =
1589
1664
  showLoader &&
1590
1665
  !(
@@ -1644,6 +1719,7 @@ function EditorContent({
1644
1719
  defaultRender={EDITOR_DEFAULT_RENDER}
1645
1720
  disablePostFx={disablePostFx}
1646
1721
  hoverStyles={EDITOR_HOVER_STYLES}
1722
+ onBreadcrumb={onViewerBreadcrumb}
1647
1723
  renderContext="editor"
1648
1724
  selectionManager="custom"
1649
1725
  >
@@ -1662,16 +1738,30 @@ function EditorContent({
1662
1738
  </Viewer>
1663
1739
  )
1664
1740
 
1741
+ const loadingOverlay = showLoader ? (
1742
+ <div className="absolute inset-0 z-50" data-scene-loading>
1743
+ {sceneLoadError ? (
1744
+ <SceneLoadFailed className="bg-background" onRetry={retrySceneLoad} />
1745
+ ) : (
1746
+ (loadingSlot ?? <SceneLoader className="bg-background" />)
1747
+ )}
1748
+ </div>
1749
+ ) : null
1750
+
1665
1751
  const viewerCanvas = (
1666
1752
  <ViewerCanvas
1667
1753
  disablePostFx={disablePostFx}
1754
+ hintBelowTopMenu={topMenuShown}
1668
1755
  hasLoadedInitialScene={hasLoadedInitialScene}
1669
1756
  isFirstPersonMode={isFirstPersonMode}
1670
1757
  isLoading={isLoading}
1758
+ isSceneDataLoading={isSceneDataLoading}
1671
1759
  isStudioMode={isStudioMode}
1672
1760
  isVersionPreviewMode={isVersionPreviewMode}
1761
+ loadingOverlay={loadingOverlay}
1673
1762
  onSceneReadyChange={handleSceneReadyChange}
1674
1763
  onThumbnailCapture={onThumbnailCapture}
1764
+ onViewerBreadcrumb={onViewerBreadcrumb}
1675
1765
  presentationsReady={presentationsReady}
1676
1766
  sceneReadyKey={sceneReadyKey}
1677
1767
  showLoader={showLoader}
@@ -1736,19 +1826,10 @@ function EditorContent({
1736
1826
  return (
1737
1827
  <>
1738
1828
  <FloorplanModeCoordinator />
1739
- {visibleLoader && (
1740
- <div className="fixed inset-0 z-60">
1741
- {sceneLoadError ? (
1742
- <SceneLoadFailed className="bg-background" onRetry={retrySceneLoad} />
1743
- ) : (
1744
- <SceneLoader className="bg-background" />
1745
- )}
1746
- </div>
1747
- )}
1748
-
1749
1829
  {!isLoading && isPreviewMode ? (
1750
1830
  <PreviewStage
1751
1831
  isFirstPersonMode={isFirstPersonMode}
1832
+ loadingOverlay={loadingOverlay}
1752
1833
  mode={previewStageMode}
1753
1834
  onModeChange={setPreviewStageMode}
1754
1835
  projectName={projectName}
@@ -1763,15 +1844,26 @@ function EditorContent({
1763
1844
  <>
1764
1845
  <SceneRegion>
1765
1846
  {!(isCaptureMode || stageOverlay) && <FloatingLevelSelector />}
1766
- {!(isVersionPreviewMode || isCaptureMode || isStudioMode) && (
1767
- <div className="pointer-events-auto">
1768
- <ActionMenu />
1769
- </div>
1770
- )}
1847
+ {!(isVersionPreviewMode || isCaptureMode || isStudioMode) &&
1848
+ (dockMenuOnTop ? (
1849
+ // Own row under the toolbar, below the level selector's opening list (z-20).
1850
+ // No transform: the menu's fixed pill must anchor to the scene region.
1851
+ <div className="pointer-events-none absolute inset-x-0 top-14 z-10 flex justify-center">
1852
+ <div className="pointer-events-auto">
1853
+ <ActionMenu inline placement="top" />
1854
+ </div>
1855
+ </div>
1856
+ ) : (
1857
+ <div className="pointer-events-auto">
1858
+ <ActionMenu placement={actionMenuPlacement} />
1859
+ </div>
1860
+ ))}
1771
1861
  {sceneOverlay}
1772
1862
  </SceneRegion>
1773
1863
  {/* The inspector and the shortcuts card share one right column. */}
1774
1864
  <RightStack
1865
+ belowTopMenu={topMenuShown}
1866
+ reserveBottomMenu={!dockMenuOnTop}
1775
1867
  helper={isCaptureMode ? null : <HelperManager />}
1776
1868
  inspector={
1777
1869
  isVersionPreviewMode || isCaptureMode || isStudioMode ? null : (
@@ -1818,21 +1910,12 @@ function EditorContent({
1818
1910
  const overlayLeft = LAYOUT_PADDING + (isSidebarCollapsed ? 8 : sidebarWidth) + LAYOUT_GAP
1819
1911
 
1820
1912
  return (
1821
- <div className="dark flex h-full w-full gap-3 bg-neutral-100 p-3 text-foreground">
1913
+ <div className="flex h-full w-full gap-3 bg-neutral-100 p-3 text-foreground">
1822
1914
  <FloorplanModeCoordinator />
1823
- {visibleLoader && (
1824
- <div className="fixed inset-0 z-60">
1825
- {sceneLoadError ? (
1826
- <SceneLoadFailed className="bg-background" onRetry={retrySceneLoad} />
1827
- ) : (
1828
- <SceneLoader className="bg-background" />
1829
- )}
1830
- </div>
1831
- )}
1832
-
1833
1915
  {!isLoading && isPreviewMode ? (
1834
1916
  <PreviewStage
1835
1917
  isFirstPersonMode={isFirstPersonMode}
1918
+ loadingOverlay={loadingOverlay}
1836
1919
  mode={previewStageMode}
1837
1920
  onModeChange={setPreviewStageMode}
1838
1921
  projectName={projectName}
@@ -1859,9 +1942,13 @@ function EditorContent({
1859
1942
  {/* Fixed UI overlays scoped to the viewer area */}
1860
1943
  <ViewerOverlays left={overlayLeft}>
1861
1944
  <div className="pointer-events-auto">
1862
- <ActionMenu />
1945
+ <ActionMenu placement={actionMenuPlacement} />
1863
1946
  </div>
1864
- <RightStack helper={<HelperManager />} inspector={<PanelManager />} />
1947
+ <RightStack
1948
+ helper={<HelperManager />}
1949
+ inspector={<PanelManager />}
1950
+ reserveBottomMenu={actionMenuPlacement !== 'top' || isMobile}
1951
+ />
1865
1952
  <RiserDiagramPanel />
1866
1953
  {isFirstPersonMode && (
1867
1954
  <FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
@@ -6,8 +6,10 @@ import { X } from 'lucide-react'
6
6
  import { useMemo } from 'react'
7
7
  import useEditor from '../../store/use-editor'
8
8
 
9
- const WASTE_COLOR = '#0ea5e9'
10
- const VENT_COLOR = '#a855f7'
9
+ // Set per theme on the panel root; the light values are darker so the 9-unit
10
+ // diameter labels read on white.
11
+ const WASTE_COLOR = 'var(--riser-waste)'
12
+ const VENT_COLOR = 'var(--riser-vent)'
11
13
  const MARKER_COLOR = '#1e293b'
12
14
  const PADDING = 32
13
15
  /** Meters → SVG units. The iso projection is in meters; scale up so a
@@ -45,14 +47,14 @@ function RiserDiagramContent() {
45
47
  const ty = diagram ? -diagram.bounds.minY * SCALE + PADDING : 0
46
48
 
47
49
  return (
48
- <div className="dark pointer-events-auto absolute top-4 right-4 z-30 flex max-h-[80vh] w-[26rem] flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 text-foreground shadow-lg backdrop-blur-xl">
50
+ <div className="pointer-events-auto absolute top-4 right-4 z-30 flex max-h-[80vh] w-[26rem] flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 text-foreground shadow-lg backdrop-blur-xl [--riser-vent:#9333ea] [--riser-waste:#0369a1] dark:[--riser-vent:#a855f7] dark:[--riser-waste:#0ea5e9]">
49
51
  <div className="flex items-center justify-between border-border/40 border-b px-4 py-2.5">
50
52
  <div className="flex flex-col">
51
53
  <span className="font-medium text-sm">Riser Diagram</span>
52
54
  <span className="text-muted-foreground text-xs">DWV plumbing isometric</span>
53
55
  </div>
54
56
  <button
55
- className="flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-white/10"
57
+ className="flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-black/6 dark:hover:bg-white/10"
56
58
  onClick={() => setRiserOpen(false)}
57
59
  >
58
60
  <X className="h-4 w-4 text-muted-foreground" />
@@ -87,18 +89,18 @@ function RiserDiagramContent() {
87
89
  <line
88
90
  className="cursor-pointer"
89
91
  onClick={() => select(line.nodeId)}
90
- stroke={color}
91
92
  strokeDasharray={line.system === 'vent' ? '5 4' : undefined}
92
93
  strokeLinecap="round"
93
94
  strokeWidth={(line.vertical ? 3.5 : 2.5) + (isSel ? 2 : 0)}
95
+ style={{ stroke: color }}
94
96
  x1={line.from[0] * SCALE}
95
97
  x2={line.to[0] * SCALE}
96
98
  y1={line.from[1] * SCALE}
97
99
  y2={line.to[1] * SCALE}
98
100
  />
99
101
  <text
100
- fill={color}
101
102
  fontSize={9}
103
+ style={{ fill: color }}
102
104
  x={((line.from[0] + line.to[0]) / 2) * SCALE + 4}
103
105
  y={((line.from[1] + line.to[1]) / 2) * SCALE - 3}
104
106
  >
@@ -115,7 +117,12 @@ function RiserDiagramContent() {
115
117
  transform={`translate(${marker.point[0] * SCALE}, ${marker.point[1] * SCALE})`}
116
118
  >
117
119
  {marker.kind === 'vent-termination' ? (
118
- <path d="M -5 0 L 0 -7 L 5 0" fill="none" stroke={VENT_COLOR} strokeWidth={2} />
120
+ <path
121
+ d="M -5 0 L 0 -7 L 5 0"
122
+ fill="none"
123
+ strokeWidth={2}
124
+ style={{ stroke: VENT_COLOR }}
125
+ />
119
126
  ) : (
120
127
  <circle fill={MARKER_COLOR} r={3} stroke={MARKER_COLOR} strokeWidth={1.5} />
121
128
  )}
@@ -28,9 +28,10 @@ import { RoomControls2D, RoomControls3D } from './room-controls'
28
28
 
29
29
  /**
30
30
  * A zone reached as a zone (hovered or selected through its area or pill, not
31
- * as a room) shows the room's outline, never a tinted volume.
31
+ * as a room, or the one a chat chip points at) shows the room's outline, never
32
+ * a tinted volume. With zones hidden, a pointed zone has nothing else to light.
32
33
  */
33
- function useZoneOutline() {
34
+ function useZoneOutline(pointedId: string | undefined) {
34
35
  const hoveredId = useViewer((state) => state.hoveredId)
35
36
  const selectedId = useViewer(
36
37
  (state) =>
@@ -39,7 +40,7 @@ function useZoneOutline() {
39
40
  )
40
41
  const unitFocused = useViewer((state) => !!state.focusedUnitId)
41
42
  const zone = useScene((state) => {
42
- for (const id of [hoveredId, selectedId]) {
43
+ for (const id of [hoveredId, pointedId, selectedId]) {
43
44
  const node = id ? state.nodes[id as AnyNodeId] : undefined
44
45
  if (node?.type === 'zone') return node
45
46
  }
@@ -53,13 +54,20 @@ function useZoneOutline() {
53
54
 
54
55
  function useRoomHighlight() {
55
56
  const room = useHighlightedRoom()
56
- const zoneOutline = useZoneOutline()
57
+ const pointedId = useViewer((state) =>
58
+ state.pointedIds.length === 1 ? state.pointedIds[0] : undefined,
59
+ )
60
+ const zoneOutline = useZoneOutline(pointedId)
57
61
  const visible = useInteractionScope(
58
62
  (state) => resolveOverlayPolicy(state.scope).conflictingControls === 'shown',
59
63
  )
60
64
  // No phase gate: site never hovers or keeps a room (`roomPickingEnabled`).
61
65
  const enabled = useEditor((state) => state.mode === 'select')
62
- return enabled && visible ? (room ?? zoneOutline) : null
66
+ if (!(enabled && visible)) return null
67
+ // A zone a chat points at shows over a kept room: it is what the person is reaching for.
68
+ if (zoneOutline && zoneOutline.zoneId === pointedId && room?.zoneId !== pointedId)
69
+ return zoneOutline
70
+ return room ?? zoneOutline
63
71
  }
64
72
 
65
73
  const noRaycast = () => {}