@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
@@ -1,45 +1,9 @@
1
1
  'use client'
2
2
 
3
- import { Icon } from '@iconify/react'
4
- import {
5
- type AnyNode,
6
- type AnyNodeId,
7
- type BuildingNode,
8
- emitter,
9
- type LevelNode,
10
- useScene,
11
- type ZoneNode,
12
- } from '@pascal-app/core'
13
- import {
14
- CLAY_PALETTE,
15
- type EdgeMode,
16
- getSceneTheme,
17
- SCENE_THEMES,
18
- useViewer,
19
- } from '@pascal-app/viewer'
20
- import {
21
- ArrowLeft,
22
- Box,
23
- Camera,
24
- Check,
25
- ChevronRight,
26
- Diamond,
27
- Layers,
28
- PenLine,
29
- Sparkles,
30
- } from 'lucide-react'
31
- import Link from 'next/link'
32
- import { useShallow } from 'zustand/react/shallow'
33
- import { getLevelDisplayName } from '../lib/level-name'
34
- import { cn } from '../lib/utils'
35
- import { ActionButton } from './ui/action-menu/action-button'
36
- import {
37
- DropdownMenu,
38
- DropdownMenuContent,
39
- DropdownMenuItem,
40
- DropdownMenuTrigger,
41
- } from './ui/primitives/dropdown-menu'
42
- import { TooltipProvider } from './ui/primitives/tooltip'
3
+ import { flushSync } from 'react-dom'
4
+ import useEditor from '../store/use-editor'
5
+ import { ViewerControlsBar } from './viewer/viewer-controls-bar'
6
+ import { ViewerSceneHeader } from './viewer/viewer-scene-header'
43
7
 
44
8
  type ProjectOwner = {
45
9
  id: string
@@ -48,187 +12,26 @@ type ProjectOwner = {
48
12
  image: string | null
49
13
  }
50
14
 
51
- const levelModeLabels: Record<'stacked' | 'exploded' | 'solo', string> = {
52
- stacked: 'Stacked',
53
- exploded: 'Exploded',
54
- solo: 'Solo',
55
- }
56
-
57
- const levelModeBadgeLabels: Record<'manual' | 'stacked' | 'exploded' | 'solo', string> = {
58
- manual: 'Stack',
59
- stacked: 'Stack',
60
- exploded: 'Exploded',
61
- solo: 'Solo',
62
- }
63
-
64
- const wallModeConfig = {
65
- up: {
66
- icon: (props: any) => (
67
- <img alt="Full Height" height={28} src="/icons/room.png" width={28} {...props} />
68
- ),
69
- label: 'Full Height',
70
- },
71
- cutaway: {
72
- icon: (props: any) => (
73
- <img alt="Cutaway" height={28} src="/icons/wallcut.png" width={28} {...props} />
74
- ),
75
- label: 'Cutaway',
76
- },
77
- down: {
78
- icon: (props: any) => (
79
- <img alt="Low" height={28} src="/icons/walllow.png" width={28} {...props} />
80
- ),
81
- label: 'Low',
82
- },
83
- }
84
-
85
- const SHADING_OPTIONS = [
86
- { id: 'solid', name: 'Solid', detail: 'Flat and fast — no ambient occlusion', icon: Box },
87
- { id: 'rendered', name: 'Rendered', detail: 'Full ambient occlusion', icon: Sparkles },
88
- ] as const
89
-
90
- function RenderModeMenu() {
91
- const shading = useViewer((s) => s.shading)
92
- const active = SHADING_OPTIONS.find((o) => o.id === shading) ?? SHADING_OPTIONS[0]
93
- const ActiveIcon = active.icon
94
- return (
95
- <DropdownMenu>
96
- <DropdownMenuTrigger asChild>
97
- <ActionButton
98
- className="text-muted-foreground/80 hover:bg-white/5 hover:text-foreground"
99
- label={`Render: ${active.name}`}
100
- size="icon"
101
- tooltipSide="top"
102
- variant="ghost"
103
- >
104
- <ActiveIcon className="h-6 w-6" />
105
- </ActionButton>
106
- </DropdownMenuTrigger>
107
- <DropdownMenuContent align="center" className="min-w-56" side="top">
108
- {SHADING_OPTIONS.map((option) => {
109
- const OptionIcon = option.icon
110
- return (
111
- <DropdownMenuItem
112
- key={option.id}
113
- onSelect={() => useViewer.getState().setShading(option.id)}
114
- >
115
- <OptionIcon />
116
- <div className="flex flex-col">
117
- <span className="text-foreground">{option.name}</span>
118
- <span className="text-muted-foreground text-xs">{option.detail}</span>
119
- </div>
120
- {shading === option.id ? <Check className="ml-auto text-foreground" /> : null}
121
- </DropdownMenuItem>
122
- )
123
- })}
124
- </DropdownMenuContent>
125
- </DropdownMenu>
126
- )
127
- }
128
-
129
- function SceneThemeMenu() {
130
- const sceneTheme = useViewer((s) => s.sceneTheme)
131
- const active = getSceneTheme(sceneTheme)
132
- return (
133
- <DropdownMenu>
134
- <DropdownMenuTrigger asChild>
135
- <ActionButton
136
- className="text-muted-foreground/80 hover:bg-white/5 hover:text-foreground"
137
- label={`Theme: ${active.name}`}
138
- size="icon"
139
- tooltipSide="top"
140
- variant="ghost"
141
- >
142
- <Icon color="currentColor" height={24} icon="lucide:swatch-book" width={24} />
143
- </ActionButton>
144
- </DropdownMenuTrigger>
145
- <DropdownMenuContent align="center" className="min-w-48" side="top">
146
- {SCENE_THEMES.map((sceneThemeOption) => {
147
- const swatches = (['wall', 'roof', 'floor', 'glazing'] as const).map(
148
- (role) => sceneThemeOption.clayTints?.[role] ?? CLAY_PALETTE[role],
149
- )
150
- return (
151
- <DropdownMenuItem
152
- key={sceneThemeOption.id}
153
- onSelect={() => useViewer.getState().setSceneTheme(sceneThemeOption.id)}
154
- >
155
- <span
156
- className="grid h-5 w-5 shrink-0 grid-cols-2 overflow-hidden rounded-sm border border-black/10"
157
- style={{ backgroundColor: sceneThemeOption.background }}
158
- >
159
- {swatches.map((color, index) => (
160
- <span
161
- key={`${sceneThemeOption.id}-${index}`}
162
- style={{ backgroundColor: color }}
163
- />
164
- ))}
165
- </span>
166
- <span className="text-foreground">{sceneThemeOption.name}</span>
167
- {sceneTheme === sceneThemeOption.id ? (
168
- <Check className="ml-auto text-foreground" />
169
- ) : null}
170
- </DropdownMenuItem>
171
- )
172
- })}
173
- </DropdownMenuContent>
174
- </DropdownMenu>
175
- )
176
- }
177
-
178
- const EDGE_OPTIONS = [
179
- { id: 'off', name: 'Off', detail: 'No edge lines' },
180
- { id: 'soft', name: 'Soft', detail: 'Faint outline of major creases' },
181
- { id: 'strong', name: 'Strong', detail: 'Crisp, opaque edge lines' },
182
- ] as const satisfies readonly { id: EdgeMode; name: string; detail: string }[]
15
+ function requestWalkthroughPointerLock() {
16
+ const canvas = document.querySelector<HTMLCanvasElement>('[data-pascal-viewer-3d] canvas')
17
+ if (!canvas) return
183
18
 
184
- function EdgesMenu() {
185
- const edges = useViewer((s) => s.edges)
186
- const active = EDGE_OPTIONS.find((o) => o.id === edges) ?? EDGE_OPTIONS[0]
187
- return (
188
- <DropdownMenu>
189
- <DropdownMenuTrigger asChild>
190
- <ActionButton
191
- className={
192
- edges === 'off'
193
- ? 'text-muted-foreground/80 hover:bg-white/5 hover:text-foreground'
194
- : 'bg-white/10 text-foreground'
195
- }
196
- label={`Edges: ${active.name}`}
197
- size="icon"
198
- tooltipSide="top"
199
- variant="ghost"
200
- >
201
- <PenLine className="h-6 w-6" />
202
- </ActionButton>
203
- </DropdownMenuTrigger>
204
- <DropdownMenuContent align="center" className="min-w-56" side="top">
205
- {EDGE_OPTIONS.map((option) => (
206
- <DropdownMenuItem
207
- key={option.id}
208
- onSelect={() => useViewer.getState().setEdges(option.id)}
209
- >
210
- <div className="flex flex-col">
211
- <span className="text-foreground">{option.name}</span>
212
- <span className="text-muted-foreground text-xs">{option.detail}</span>
213
- </div>
214
- {edges === option.id ? <Check className="ml-auto text-foreground" /> : null}
215
- </DropdownMenuItem>
216
- ))}
217
- </DropdownMenuContent>
218
- </DropdownMenu>
219
- )
220
- }
221
-
222
- const getNodeName = (node: AnyNode): string => {
223
- if ('name' in node && node.name) return node.name
224
- if (node.type === 'wall') return 'Wall'
225
- if (node.type === 'fence') return 'Fence'
226
- if (node.type === 'item') return (node as { asset: { name: string } }).asset?.name || 'Item'
227
- if (node.type === 'slab') return 'Slab'
228
- if (node.type === 'ceiling') return 'Ceiling'
229
- if (node.type === 'roof') return 'Roof'
230
- if (node.type === 'roof-segment') return 'Roof Segment'
231
- return node.type
19
+ if (!canvas.hasAttribute('tabindex')) {
20
+ canvas.tabIndex = -1
21
+ }
22
+ canvas.focus({ preventScroll: true })
23
+
24
+ if (document.pointerLockElement === canvas) return
25
+
26
+ try {
27
+ // The request can also reject ASYNC (browser cooldown after a recent
28
+ // unlock) — swallow it like the P-resume path; clicking the canvas
29
+ // re-requests once the cooldown passes.
30
+ const result = canvas.requestPointerLock?.() as Promise<void> | undefined
31
+ if (result && typeof result.catch === 'function') result.catch(() => {})
32
+ } catch {
33
+ return
34
+ }
232
35
  }
233
36
 
234
37
  interface ViewerOverlayProps {
@@ -245,379 +48,16 @@ export const ViewerOverlay = ({
245
48
  canShowScans = true,
246
49
  canShowGuides = true,
247
50
  onBack,
248
- }: ViewerOverlayProps) => {
249
- const selection = useViewer((s) => s.selection)
250
- const showScans = useViewer((s) => s.showScans)
251
- const showGuides = useViewer((s) => s.showGuides)
252
- const cameraMode = useViewer((s) => s.cameraMode)
253
- const levelMode = useViewer((s) => s.levelMode)
254
- const wallMode = useViewer((s) => s.wallMode)
255
-
256
- // Subscribe only to the specific nodes we read so that creating an unrelated
257
- // node elsewhere in the scene doesn't re-render this overlay.
258
- const firstSelectedId = selection.selectedIds[0] ?? null
259
- const building = useScene((s) =>
260
- selection.buildingId ? (s.nodes[selection.buildingId] as BuildingNode | undefined) : null,
261
- )
262
- const level = useScene((s) =>
263
- selection.levelId ? (s.nodes[selection.levelId] as LevelNode | undefined) : null,
264
- )
265
- const zone = useScene((s) =>
266
- selection.zoneId ? (s.nodes[selection.zoneId] as ZoneNode | undefined) : null,
267
- )
268
- const selectedNode = useScene((s) =>
269
- firstSelectedId ? (s.nodes[firstSelectedId as AnyNodeId] as AnyNode | undefined) : null,
270
- )
271
- const levels = useScene(
272
- useShallow((s) => {
273
- if (!building) return []
274
- return building.children
275
- .map((id) => s.nodes[id as AnyNodeId] as LevelNode | undefined)
276
- .filter((n): n is LevelNode => n?.type === 'level')
277
- .sort((a, b) => a.level - b.level)
278
- }),
279
- )
280
-
281
- const handleLevelClick = (levelId: LevelNode['id']) => {
282
- // When switching levels, deselect zone and items
283
- useViewer.getState().setSelection({ levelId })
284
- }
285
-
286
- const handleBreadcrumbClick = (depth: 'root' | 'building' | 'level' | 'zone') => {
287
- switch (depth) {
288
- case 'root':
289
- useViewer.getState().resetSelection()
290
- break
291
- case 'building':
292
- useViewer.getState().setSelection({ levelId: null })
293
- break
294
- case 'level':
295
- useViewer.getState().setSelection({ zoneId: null })
296
- break
297
- }
298
- }
299
-
300
- return (
301
- <>
302
- {/* Unified top-left card */}
303
- <div className="dark absolute top-4 left-4 z-20 flex flex-col gap-3 text-foreground">
304
- <div className="pointer-events-auto flex min-w-[200px] flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 shadow-lg backdrop-blur-xl transition-colors duration-200 ease-out">
305
- {/* Project info + back */}
306
- <div className="flex items-center gap-3 px-3 py-2.5">
307
- {onBack ? (
308
- <button
309
- className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-white/10"
310
- onClick={onBack}
311
- >
312
- <ArrowLeft className="h-4 w-4 text-muted-foreground" />
313
- </button>
314
- ) : (
315
- <Link
316
- className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-white/10"
317
- href="/"
318
- >
319
- <ArrowLeft className="h-4 w-4 text-muted-foreground" />
320
- </Link>
321
- )}
322
- <div className="min-w-0">
323
- <div className="truncate font-medium text-foreground text-sm">
324
- {projectName || 'Untitled'}
325
- </div>
326
- {owner?.username && (
327
- <Link
328
- className="text-muted-foreground text-xs transition-colors hover:text-foreground"
329
- href={`/u/${owner.username}`}
330
- >
331
- @{owner.username}
332
- </Link>
333
- )}
334
- </div>
335
- </div>
336
-
337
- {/* Breadcrumb — only shown when navigated into a building */}
338
- {building && (
339
- <div className="border-border/40 border-t px-3 py-2">
340
- <div className="flex items-center gap-1.5 text-xs">
341
- <button
342
- className="text-muted-foreground transition-colors hover:text-foreground"
343
- onClick={() => handleBreadcrumbClick('root')}
344
- >
345
- Site
346
- </button>
347
-
348
- {building && (
349
- <>
350
- <ChevronRight className="h-3 w-3 text-muted-foreground/50" />
351
- <button
352
- className={`truncate transition-colors ${level ? 'text-muted-foreground hover:text-foreground' : 'font-medium text-foreground'}`}
353
- onClick={() => handleBreadcrumbClick('building')}
354
- >
355
- {building.name || 'Building'}
356
- </button>
357
- </>
358
- )}
359
-
360
- {level && (
361
- <>
362
- <ChevronRight className="h-3 w-3 text-muted-foreground/50" />
363
- <button
364
- className={`truncate transition-colors ${zone ? 'text-muted-foreground hover:text-foreground' : 'font-medium text-foreground'}`}
365
- onClick={() => handleBreadcrumbClick('level')}
366
- >
367
- {getLevelDisplayName(level)}
368
- </button>
369
- </>
370
- )}
371
-
372
- {zone && (
373
- <>
374
- <ChevronRight className="h-3 w-3 text-muted-foreground/50" />
375
- <span
376
- className={`truncate transition-colors ${selectedNode ? 'text-muted-foreground' : 'font-medium text-foreground'}`}
377
- >
378
- {zone.name}
379
- </span>
380
- </>
381
- )}
382
-
383
- {selectedNode && zone && (
384
- <>
385
- <ChevronRight className="h-3 w-3 text-muted-foreground/50" />
386
- <span className="truncate font-medium text-foreground">
387
- {getNodeName(selectedNode)}
388
- </span>
389
- </>
390
- )}
391
- </div>
392
- </div>
393
- )}
394
- </div>
395
-
396
- {/* Level List (only when building is selected) */}
397
- {building && levels.length > 0 && (
398
- <div className="pointer-events-auto flex w-48 flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 py-1 shadow-lg backdrop-blur-xl transition-colors duration-200 ease-out">
399
- <span className="px-3 py-2 font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
400
- Levels
401
- </span>
402
- <div className="flex flex-col">
403
- {levels.map((lvl) => {
404
- const isSelected = lvl.id === selection.levelId
405
- return (
406
- <button
407
- className={cn(
408
- 'group/row relative flex h-8 w-full cursor-pointer select-none items-center border-border/50 border-r border-r-transparent border-b px-3 text-sm transition-all duration-200',
409
- isSelected
410
- ? 'border-r-3 border-r-white bg-accent/50 text-foreground'
411
- : 'text-muted-foreground hover:bg-accent/30 hover:text-foreground',
412
- )}
413
- key={lvl.id}
414
- onClick={() => handleLevelClick(lvl.id)}
415
- >
416
- <div className="flex min-w-0 flex-1 items-center gap-2">
417
- <span
418
- className={cn(
419
- 'flex h-4 w-4 shrink-0 items-center justify-center transition-all duration-200',
420
- !isSelected && 'opacity-60 grayscale',
421
- )}
422
- >
423
- <Layers className="h-3.5 w-3.5" />
424
- </span>
425
- <div className="min-w-0 flex-1 truncate text-left">
426
- {lvl.name || `Level ${lvl.level}`}
427
- </div>
428
- </div>
429
- </button>
430
- )
431
- })}
432
- </div>
433
- </div>
434
- )}
435
- </div>
436
-
437
- {/* Controls Panel - Bottom Center */}
438
- <div className="dark absolute bottom-6 left-1/2 z-20 -translate-x-1/2 text-foreground">
439
- <TooltipProvider delayDuration={0}>
440
- <div className="pointer-events-auto flex h-14 flex-row items-center justify-center gap-1.5 rounded-2xl border border-border/40 bg-background/95 p-1.5 shadow-lg backdrop-blur-xl transition-colors duration-200 ease-out">
441
- {/* Scans and Guides Visibility */}
442
- {canShowScans && (
443
- <ActionButton
444
- className={
445
- showScans
446
- ? 'bg-white/10'
447
- : 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0'
448
- }
449
- label={`Scans: ${showScans ? 'Visible' : 'Hidden'}`}
450
- onClick={() => useViewer.getState().setShowScans(!showScans)}
451
- size="icon"
452
- tooltipSide="top"
453
- variant="ghost"
454
- >
455
- <img
456
- alt="Scans"
457
- className="h-[28px] w-[28px] object-contain"
458
- src="/icons/mesh.png"
459
- />
460
- </ActionButton>
461
- )}
462
-
463
- {canShowGuides && (
464
- <ActionButton
465
- className={
466
- showGuides
467
- ? 'bg-white/10'
468
- : 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0'
469
- }
470
- label={`Guides: ${showGuides ? 'Visible' : 'Hidden'}`}
471
- onClick={() => useViewer.getState().setShowGuides(!showGuides)}
472
- size="icon"
473
- tooltipSide="top"
474
- variant="ghost"
475
- >
476
- <img
477
- alt="Guides"
478
- className="h-[28px] w-[28px] object-contain"
479
- src="/icons/floorplan.png"
480
- />
481
- </ActionButton>
482
- )}
483
-
484
- {(canShowScans || canShowGuides) && <div className="mx-1 h-5 w-px bg-border/40" />}
485
-
486
- {/* Camera Mode */}
487
- <ActionButton
488
- className={
489
- cameraMode === 'orthographic'
490
- ? 'bg-violet-500/20 text-violet-400'
491
- : 'hover:bg-white/5 hover:text-violet-400'
492
- }
493
- label={`Camera: ${cameraMode === 'perspective' ? 'Perspective' : 'Orthographic'}`}
494
- onClick={() =>
495
- useViewer
496
- .getState()
497
- .setCameraMode(cameraMode === 'perspective' ? 'orthographic' : 'perspective')
498
- }
499
- size="icon"
500
- tooltipSide="top"
501
- variant="ghost"
502
- >
503
- <Camera className="h-6 w-6" />
504
- </ActionButton>
505
-
506
- <RenderModeMenu />
507
-
508
- <SceneThemeMenu />
509
-
510
- <EdgesMenu />
511
-
512
- {/* Level Mode */}
513
- <ActionButton
514
- className={cn(
515
- 'p-0',
516
- levelMode === 'stacked' || levelMode === 'manual'
517
- ? 'text-muted-foreground/80 hover:bg-white/5 hover:text-foreground'
518
- : 'bg-white/10 text-foreground',
519
- )}
520
- label={`Levels: ${levelMode === 'manual' ? 'Manual' : levelModeLabels[levelMode as keyof typeof levelModeLabels]}`}
521
- onClick={() => {
522
- if (levelMode === 'manual') return useViewer.getState().setLevelMode('stacked')
523
- const modes: ('stacked' | 'exploded' | 'solo')[] = ['stacked', 'exploded', 'solo']
524
- const nextIndex = (modes.indexOf(levelMode as any) + 1) % modes.length
525
- useViewer.getState().setLevelMode(modes[nextIndex] ?? 'stacked')
526
- }}
527
- size="icon"
528
- tooltipSide="top"
529
- variant="ghost"
530
- >
531
- <span className="relative flex h-full w-full items-center justify-center pb-1">
532
- {levelMode === 'solo' && <Diamond className="h-6 w-6" />}
533
- {levelMode === 'exploded' && (
534
- <Icon color="currentColor" height={24} icon="charm:stack-pop" width={24} />
535
- )}
536
- {(levelMode === 'stacked' || levelMode === 'manual') && (
537
- <Icon color="currentColor" height={24} icon="charm:stack-push" width={24} />
538
- )}
539
- <span
540
- aria-hidden="true"
541
- className="pointer-events-none absolute right-1 bottom-1 left-1 rounded border border-border/50 bg-background/70 px-0.5 py-[2px] text-center font-medium font-pixel text-[8px] text-foreground/85 leading-none tracking-[-0.02em] backdrop-blur-sm"
542
- >
543
- {levelModeBadgeLabels[levelMode]}
544
- </span>
545
- </span>
546
- </ActionButton>
547
-
548
- {/* Wall Mode */}
549
- <ActionButton
550
- className={
551
- wallMode !== 'cutaway'
552
- ? 'bg-white/10'
553
- : 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0'
554
- }
555
- label={`Walls: ${wallModeConfig[wallMode as keyof typeof wallModeConfig].label}`}
556
- onClick={() => {
557
- const modes: ('cutaway' | 'up' | 'down')[] = ['cutaway', 'up', 'down']
558
- const nextIndex = (modes.indexOf(wallMode as any) + 1) % modes.length
559
- useViewer.getState().setWallMode(modes[nextIndex] ?? 'cutaway')
560
- }}
561
- size="icon"
562
- tooltipSide="top"
563
- variant="ghost"
564
- >
565
- {(() => {
566
- const Icon = wallModeConfig[wallMode as keyof typeof wallModeConfig].icon
567
- return <Icon className="h-[28px] w-[28px]" />
568
- })()}
569
- </ActionButton>
570
-
571
- <div className="mx-1 h-5 w-px bg-border/40" />
572
-
573
- {/* Camera Actions */}
574
- <ActionButton
575
- className="group hidden hover:bg-white/5 sm:inline-flex"
576
- label="Orbit Left"
577
- onClick={() => emitter.emit('camera-controls:orbit-ccw')}
578
- size="icon"
579
- tooltipSide="top"
580
- variant="ghost"
581
- >
582
- <img
583
- alt="Orbit Left"
584
- className="h-[28px] w-[28px] -scale-x-100 object-contain opacity-70 transition-opacity group-hover:opacity-100"
585
- src="/icons/rotate.png"
586
- />
587
- </ActionButton>
588
-
589
- <ActionButton
590
- className="group hidden hover:bg-white/5 sm:inline-flex"
591
- label="Orbit Right"
592
- onClick={() => emitter.emit('camera-controls:orbit-cw')}
593
- size="icon"
594
- tooltipSide="top"
595
- variant="ghost"
596
- >
597
- <img
598
- alt="Orbit Right"
599
- className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
600
- src="/icons/rotate.png"
601
- />
602
- </ActionButton>
603
-
604
- <ActionButton
605
- className="group hover:bg-white/5"
606
- label="Top View"
607
- onClick={() => emitter.emit('camera-controls:top-view')}
608
- size="icon"
609
- tooltipSide="top"
610
- variant="ghost"
611
- >
612
- <img
613
- alt="Top View"
614
- className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
615
- src="/icons/topview.png"
616
- />
617
- </ActionButton>
618
- </div>
619
- </TooltipProvider>
620
- </div>
621
- </>
622
- )
623
- }
51
+ }: ViewerOverlayProps) => (
52
+ <>
53
+ <ViewerSceneHeader onBack={onBack} owner={owner} projectName={projectName} />
54
+ <ViewerControlsBar
55
+ canShowGuides={canShowGuides}
56
+ canShowScans={canShowScans}
57
+ onWalkthroughToggle={() => {
58
+ flushSync(() => useEditor.getState().setFirstPersonMode(true))
59
+ requestWalkthroughPointerLock()
60
+ }}
61
+ />
62
+ </>
63
+ )
@@ -4,13 +4,18 @@ import { sceneRegistry, useScene, type ZoneNode } from '@pascal-app/core'
4
4
  import { useViewer } from '@pascal-app/viewer'
5
5
  import { useFrame } from '@react-three/fiber'
6
6
  import type { Mesh } from 'three'
7
+ import { resolveOverlayPolicy } from '../lib/interaction/overlay-policy'
7
8
  import useEditor from '../store/use-editor'
9
+ import useInteractionScope from '../store/use-interaction-scope'
8
10
 
9
11
  export const ViewerZoneSystem = () => {
10
12
  useFrame(() => {
11
13
  const { levelId, zoneId } = useViewer.getState().selection
12
14
  const structureLayer = useEditor.getState().structureLayer
13
15
  const nodes = useScene.getState().nodes
16
+ // During any active interaction zone labels step back entirely (Sims-light).
17
+ const zoneLabelsHidden =
18
+ resolveOverlayPolicy(useInteractionScope.getState().scope).zoneLabels === 'hidden'
14
19
 
15
20
  sceneRegistry.byType.zone!.forEach((id) => {
16
21
  const obj = sceneRegistry.nodes.get(id)
@@ -35,7 +40,7 @@ export const ViewerZoneSystem = () => {
35
40
  })
36
41
 
37
42
  // Labels: always visible on the current level (regardless of mode or zone selection)
38
- const showLabel = !!levelId && isOnSelectedLevel
43
+ const showLabel = !zoneLabelsHidden && !!levelId && isOnSelectedLevel
39
44
  const targetOpacity = showLabel ? '1' : '0'
40
45
  const labelEl = document.getElementById(`${id}-label`)
41
46
  if (labelEl && labelEl.style.opacity !== targetOpacity) {