@pascal-app/editor 1.0.6 → 1.1.0

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 (382) hide show
  1. package/package.json +18 -15
  2. package/src/components/editor/action-menu-button.tsx +4 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
  4. package/src/components/editor/bake-thumbnail.tsx +3 -2
  5. package/src/components/editor/custom-camera-controls.tsx +110 -42
  6. package/src/components/editor/duplicate-node.ts +2 -1
  7. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  8. package/src/components/editor/first-person-controls.tsx +77 -68
  9. package/src/components/editor/fit-scene-framing.test.ts +318 -0
  10. package/src/components/editor/fit-scene-framing.ts +142 -0
  11. package/src/components/editor/floating-action-menu.tsx +31 -11
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floor-openings-overlay.tsx +1 -1
  14. package/src/components/editor/floor-region-controls.tsx +2 -2
  15. package/src/components/editor/floorplan-panel.test.ts +220 -0
  16. package/src/components/editor/floorplan-panel.tsx +458 -140
  17. package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
  18. package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
  19. package/src/components/editor/footprint-height-handle.tsx +2 -2
  20. package/src/components/editor/grid.tsx +24 -15
  21. package/src/components/editor/group-actions.ts +19 -1
  22. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  23. package/src/components/editor/group-move-3d.ts +27 -2
  24. package/src/components/editor/group-selection-box-3d.tsx +4 -1
  25. package/src/components/editor/group-transform-shared.ts +23 -1
  26. package/src/components/editor/index.tsx +150 -20
  27. package/src/components/editor/inspector-extension-actions.tsx +36 -0
  28. package/src/components/editor/localized-html.tsx +22 -0
  29. package/src/components/editor/measurement-pill.tsx +1 -1
  30. package/src/components/editor/node-action-menu.tsx +46 -12
  31. package/src/components/editor/node-arrow-handles.tsx +2 -3
  32. package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
  33. package/src/components/editor/open-wall-ends-hint.tsx +1 -1
  34. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  35. package/src/components/editor/quick-measurement-card.tsx +1 -1
  36. package/src/components/editor/registry-action-contributions.test.tsx +67 -6
  37. package/src/components/editor/registry-action-contributions.tsx +115 -15
  38. package/src/components/editor/riser-diagram-panel.tsx +59 -9
  39. package/src/components/editor/room-controls.tsx +4 -3
  40. package/src/components/editor/room-handles-2d.tsx +158 -7
  41. package/src/components/editor/room-handles.tsx +244 -54
  42. package/src/components/editor/room-highlight.tsx +2 -0
  43. package/src/components/editor/room-labels.tsx +33 -17
  44. package/src/components/editor/selection-manager.tsx +15 -102
  45. package/src/components/editor/separator-end-grip.tsx +74 -0
  46. package/src/components/editor/site-edge-labels.tsx +7 -3
  47. package/src/components/editor/thumbnail-generator.test.ts +15 -6
  48. package/src/components/editor/thumbnail-generator.tsx +26 -1
  49. package/src/components/editor/use-floorplan-background-placement.ts +55 -52
  50. package/src/components/editor/views/view-bar.tsx +111 -26
  51. package/src/components/editor/wall-measurement-label.tsx +31 -15
  52. package/src/components/editor/wall-move-side-handles.tsx +15 -7
  53. package/src/components/editor/wall-pull-grip.tsx +193 -0
  54. package/src/components/editor/wall-region-handles.tsx +2 -2
  55. package/src/components/editor/wall-snap-beacon-layer.tsx +134 -33
  56. package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
  57. package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
  58. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
  59. package/src/components/editor-2d/floorplan-group-move.tsx +32 -4
  60. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
  61. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +33 -24
  62. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +12 -5
  63. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
  64. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +29 -5
  65. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
  66. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
  67. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
  68. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
  69. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
  70. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
  71. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +34 -1
  72. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +211 -39
  73. package/src/components/procedural-items/inspector.tsx +59 -12
  74. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
  75. package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
  76. package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
  77. package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
  78. package/src/components/systems/roof/roof-edit-system.tsx +30 -19
  79. package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
  80. package/src/components/tools/distribution/distribution-run-tool.tsx +200 -58
  81. package/src/components/tools/item/move-tool.tsx +1 -6
  82. package/src/components/tools/item/placement-history.test.tsx +1 -1
  83. package/src/components/tools/item/placement-strategies.test.ts +64 -1
  84. package/src/components/tools/item/placement-strategies.ts +5 -9
  85. package/src/components/tools/item/use-placement-coordinator.tsx +31 -35
  86. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
  87. package/src/components/tools/registry/item-surface-move.test.ts +11 -0
  88. package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
  89. package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
  90. package/src/components/tools/select/select-candidates.test.ts +45 -0
  91. package/src/components/tools/select/select-candidates.ts +13 -2
  92. package/src/components/tools/shared/cursor-sphere.tsx +10 -3
  93. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  94. package/src/components/tools/shared/placement-box.tsx +2 -6
  95. package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
  96. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  97. package/src/components/tools/shared/segment-angle.ts +0 -4
  98. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  99. package/src/components/tools/tool-manager.tsx +1 -24
  100. package/src/components/tools/wall/wall-drafting.test.ts +299 -23
  101. package/src/components/tools/wall/wall-drafting.ts +132 -24
  102. package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
  103. package/src/components/tools/wall/wall-pull-snap.ts +563 -0
  104. package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
  105. package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
  106. package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
  107. package/src/components/tools/wall/wall-typed-entry.ts +70 -0
  108. package/src/components/tools/zone/zone-tool.tsx +3 -3
  109. package/src/components/ui/action-menu/action-button.tsx +2 -0
  110. package/src/components/ui/action-menu/camera-actions.tsx +8 -7
  111. package/src/components/ui/action-menu/control-modes.tsx +9 -5
  112. package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
  113. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
  114. package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
  115. package/src/components/ui/action-menu/index.tsx +1 -0
  116. package/src/components/ui/action-menu/measurement-control.tsx +35 -29
  117. package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
  118. package/src/components/ui/action-menu/structure-tools.tsx +25 -23
  119. package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
  120. package/src/components/ui/action-menu/view-toggles.tsx +256 -51
  121. package/src/components/ui/build-panel/build-panel.tsx +163 -25
  122. package/src/components/ui/command-palette/editor-commands.tsx +38 -11
  123. package/src/components/ui/controls/finish-swatch.tsx +45 -19
  124. package/src/components/ui/controls/material-picker.tsx +24 -17
  125. package/src/components/ui/controls/metric-control.test.ts +52 -0
  126. package/src/components/ui/controls/metric-control.tsx +62 -24
  127. package/src/components/ui/controls/segmented-control.tsx +5 -2
  128. package/src/components/ui/controls/slider-control.test.tsx +23 -0
  129. package/src/components/ui/controls/slider-control.tsx +47 -19
  130. package/src/components/ui/controls/tool-options-panel.tsx +74 -44
  131. package/src/components/ui/floating-level-selector.tsx +100 -48
  132. package/src/components/ui/helpers/building-helper.tsx +5 -3
  133. package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
  134. package/src/components/ui/helpers/helper-manager.test.tsx +106 -3
  135. package/src/components/ui/helpers/helper-manager.tsx +136 -57
  136. package/src/components/ui/helpers/item-helper.tsx +6 -4
  137. package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
  138. package/src/components/ui/level-duplicate-dialog.tsx +32 -23
  139. package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
  140. package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
  141. package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
  142. package/src/components/ui/panels/floor-foundation-panel.tsx +114 -39
  143. package/src/components/ui/panels/floor-opening-panel.tsx +74 -26
  144. package/src/components/ui/panels/inspector-extensions.tsx +66 -0
  145. package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
  146. package/src/components/ui/panels/mobile-selection-bar.tsx +34 -28
  147. package/src/components/ui/panels/multi-field-value.test.ts +27 -0
  148. package/src/components/ui/panels/multi-field-value.ts +20 -1
  149. package/src/components/ui/panels/multi-height-mode.tsx +11 -10
  150. package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
  151. package/src/components/ui/panels/multi-selection-panel.tsx +76 -36
  152. package/src/components/ui/panels/node-display.ts +35 -24
  153. package/src/components/ui/panels/panel-manager.tsx +9 -5
  154. package/src/components/ui/panels/panel-wrapper.tsx +63 -127
  155. package/src/components/ui/panels/parametric-field-control.tsx +13 -10
  156. package/src/components/ui/panels/parametric-inspector.tsx +18 -12
  157. package/src/components/ui/panels/reference-panel.tsx +51 -35
  158. package/src/components/ui/panels/room-construction-rows.tsx +465 -66
  159. package/src/components/ui/panels/room-name-options.test.ts +36 -3
  160. package/src/components/ui/panels/room-name-options.ts +110 -11
  161. package/src/components/ui/panels/room-openings-field.tsx +22 -13
  162. package/src/components/ui/panels/room-panel.tsx +207 -100
  163. package/src/components/ui/panels/shape-choice.tsx +7 -5
  164. package/src/components/ui/panels/wall-region-list.tsx +7 -7
  165. package/src/components/ui/primitives/shortcut-token.tsx +10 -6
  166. package/src/components/ui/right-stack.test.ts +13 -6
  167. package/src/components/ui/right-stack.tsx +21 -10
  168. package/src/components/ui/sidebar/icon-rail.tsx +11 -9
  169. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
  170. package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
  171. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
  172. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
  173. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
  174. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
  175. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -3
  176. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
  177. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
  178. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
  179. package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
  180. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
  181. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
  182. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
  183. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
  184. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
  185. package/src/components/ui/sidebar/panels/site-panel/index.tsx +118 -72
  186. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
  187. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
  188. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
  189. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
  190. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
  191. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
  192. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -4
  193. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
  194. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
  195. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
  196. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
  197. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
  198. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
  199. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
  200. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
  201. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
  202. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +19 -9
  203. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
  204. package/src/components/ui/sidebar/tab-bar.tsx +4 -1
  205. package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
  206. package/src/components/ui/snap-target-badge.tsx +7 -6
  207. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
  208. package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
  209. package/src/components/viewer/floorplan-compass-button.tsx +12 -3
  210. package/src/components/viewer/floorplan-preview.tsx +27 -19
  211. package/src/components/viewer/render-settings-menu.tsx +294 -0
  212. package/src/components/viewer/sky-control.tsx +710 -0
  213. package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
  214. package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
  215. package/src/components/viewer/viewer-controls-bar.tsx +278 -260
  216. package/src/components/viewer/viewer-hover-label.tsx +12 -4
  217. package/src/components/viewer/viewer-rooms.tsx +62 -5
  218. package/src/components/viewer/viewer-scene-header.tsx +28 -17
  219. package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
  220. package/src/components/viewer-overlay.tsx +8 -0
  221. package/src/components/walkthrough-hud.tsx +75 -2
  222. package/src/hooks/use-keyboard.test.ts +85 -4
  223. package/src/hooks/use-keyboard.ts +65 -7
  224. package/src/hooks/use-selected-room.ts +12 -1
  225. package/src/i18n/floor-footprint-name.ts +30 -0
  226. package/src/i18n/level-name.ts +39 -0
  227. package/src/i18n/locales/en/messages.po +15849 -0
  228. package/src/i18n/locales/en/messages.ts +1 -0
  229. package/src/i18n/locales/en-XA/messages.ts +1 -0
  230. package/src/i18n/locales/es/messages.po +39331 -0
  231. package/src/i18n/locales/es/messages.ts +1 -0
  232. package/src/i18n/locales/fr/messages.po +39331 -0
  233. package/src/i18n/locales/fr/messages.ts +1 -0
  234. package/src/i18n/locales/ja/messages.po +39331 -0
  235. package/src/i18n/locales/ja/messages.ts +1 -0
  236. package/src/i18n/locales/zh-CN/messages.po +39331 -0
  237. package/src/i18n/locales/zh-CN/messages.ts +1 -0
  238. package/src/i18n/locales.test.ts +63 -0
  239. package/src/i18n/locales.ts +32 -0
  240. package/src/i18n/message-slot.tsx +25 -0
  241. package/src/i18n/node-messages.ts +169 -0
  242. package/src/i18n/pseudo-locale.ts +17 -0
  243. package/src/i18n/published-entry.test.ts +17 -0
  244. package/src/i18n/scene-theme-name.ts +16 -0
  245. package/src/index.tsx +224 -45
  246. package/src/lib/add-storey.test.ts +3 -3
  247. package/src/lib/contextual-help.test.ts +20 -1
  248. package/src/lib/contextual-help.ts +101 -19
  249. package/src/lib/continuation.ts +38 -11
  250. package/src/lib/elevation-guides.ts +21 -6
  251. package/src/lib/elevator-support.ts +1 -72
  252. package/src/lib/floor-footprints.test.ts +3 -2
  253. package/src/lib/floor-footprints.ts +1 -9
  254. package/src/lib/floor-foundation-support.test.ts +15 -6
  255. package/src/lib/floor-opening-draft.ts +1 -1
  256. package/src/lib/floor-region-session.ts +1 -1
  257. package/src/lib/floorplan/dimension-drive.test.ts +36 -2
  258. package/src/lib/floorplan/dimension-drive.ts +68 -3
  259. package/src/lib/floorplan/floorplan-export.test.ts +16 -5
  260. package/src/lib/floorplan/floorplan-export.tsx +13 -16
  261. package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
  262. package/src/lib/floorplan/floorplan-extension.ts +37 -18
  263. package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
  264. package/src/lib/floorplan/floorplan-mode.ts +14 -12
  265. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
  266. package/src/lib/floorplan/floorplan-readonly.ts +3 -3
  267. package/src/lib/floorplan/open-wall-ends.ts +2 -1
  268. package/src/lib/floorplan/schedules.ts +10 -6
  269. package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
  270. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
  271. package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
  272. package/src/lib/geometry-script/author.test.ts +114 -0
  273. package/src/lib/geometry-script/author.ts +55 -23
  274. package/src/lib/geometry-script/client.ts +3 -0
  275. package/src/lib/geometry-script/finish-scripts.ts +7 -7
  276. package/src/lib/gesture-lifecycle.ts +3 -1
  277. package/src/lib/glb-export.test.ts +327 -0
  278. package/src/lib/glb-export.ts +70 -2
  279. package/src/lib/grid-frame.test.ts +56 -3
  280. package/src/lib/grid-frame.ts +28 -1
  281. package/src/lib/history.test.ts +2 -1
  282. package/src/lib/hud-title.ts +48 -24
  283. package/src/lib/ifc-export.test.ts +41 -1
  284. package/src/lib/inspector-card-mode.test.ts +3 -31
  285. package/src/lib/inspector-card-mode.ts +5 -10
  286. package/src/lib/inspector-expanded.test.ts +13 -0
  287. package/src/lib/inspector-expanded.ts +13 -1
  288. package/src/lib/interaction/scope.ts +1 -0
  289. package/src/lib/keyboard-pan.test.ts +7 -0
  290. package/src/lib/keyboard-pan.ts +9 -2
  291. package/src/lib/layer-thickness.test.ts +22 -0
  292. package/src/lib/layer-thickness.ts +79 -0
  293. package/src/lib/level-selection.test.ts +18 -2
  294. package/src/lib/level-selection.ts +31 -8
  295. package/src/lib/linear-display.ts +1 -1
  296. package/src/lib/live-gesture-round-trip.test.ts +1 -1
  297. package/src/lib/lot/drop-in.test.ts +59 -1
  298. package/src/lib/lot/drop-in.ts +42 -50
  299. package/src/lib/material-swatch-style.ts +24 -0
  300. package/src/lib/measurement-label.test.ts +1 -1
  301. package/src/lib/measurement-parser.ts +7 -3
  302. package/src/lib/measurements.test.ts +24 -6
  303. package/src/lib/measurements.ts +1 -203
  304. package/src/lib/mezzanine-selection.test.ts +3 -1
  305. package/src/lib/node-action-movement.test.tsx +115 -0
  306. package/src/lib/node-action-movement.ts +2 -0
  307. package/src/lib/orbit-near-plane.test.ts +12 -0
  308. package/src/lib/orbit-near-plane.ts +13 -0
  309. package/src/lib/paint-regions.test.ts +8 -5
  310. package/src/lib/paint-scope.test.ts +53 -0
  311. package/src/lib/paint-scope.ts +18 -4
  312. package/src/lib/panel-rows.ts +17 -8
  313. package/src/lib/placement-metadata.ts +4 -0
  314. package/src/lib/point-ask/describe-target.ts +6 -8
  315. package/src/lib/point-ask/region.ts +1 -1
  316. package/src/lib/print-shell-compiler.ts +12 -1
  317. package/src/lib/print-wall-solids.test.ts +11 -0
  318. package/src/lib/print-wall-solids.ts +9 -0
  319. package/src/lib/reference-scale.ts +83 -0
  320. package/src/lib/room-assembly-overlay.ts +1 -1
  321. package/src/lib/room-built-on.test.ts +28 -17
  322. package/src/lib/room-built-on.ts +21 -17
  323. package/src/lib/room-construction-commands.ts +3 -2
  324. package/src/lib/room-construction.test.ts +28 -1
  325. package/src/lib/room-construction.ts +14 -0
  326. package/src/lib/room-controls-phase6.test.ts +141 -1
  327. package/src/lib/room-handle-drag.ts +554 -58
  328. package/src/lib/room-name-catalog.test.ts +8 -0
  329. package/src/lib/room-name-catalog.ts +8 -1
  330. package/src/lib/room-name-messages.test.ts +132 -0
  331. package/src/lib/room-name-messages.ts +147 -0
  332. package/src/lib/room-name-storage.test.ts +334 -0
  333. package/src/lib/room-push-dimensions.test.ts +38 -1
  334. package/src/lib/room-push-dimensions.ts +226 -2
  335. package/src/lib/room-selection.test.ts +23 -6
  336. package/src/lib/scene-bounds.test.ts +144 -0
  337. package/src/lib/scene-bounds.ts +79 -15
  338. package/src/lib/scene-clipboard.test.ts +70 -49
  339. package/src/lib/scene-clipboard.ts +11 -5
  340. package/src/lib/selection-cycle.ts +63 -0
  341. package/src/lib/separator-edit.test.ts +431 -0
  342. package/src/lib/separator-edit.ts +179 -0
  343. package/src/lib/separator-lines.ts +38 -0
  344. package/src/lib/sfx-bus.ts +27 -1
  345. package/src/lib/sfx-player.ts +16 -0
  346. package/src/lib/snapping-mode.test.ts +23 -1
  347. package/src/lib/tool-cursor-icon.ts +5 -1
  348. package/src/lib/tool-shortcuts.ts +130 -0
  349. package/src/lib/viewer-selection.test.ts +28 -24
  350. package/src/lib/viewer-selection.ts +21 -6
  351. package/src/lib/wall-draw-variant.test.ts +7 -6
  352. package/src/lib/wall-draw-variant.ts +107 -6
  353. package/src/lib/wall-nudge.test.ts +44 -0
  354. package/src/lib/wall-nudge.ts +131 -0
  355. package/src/lib/wall-parallel-snap.test.ts +83 -0
  356. package/src/lib/wall-parallel-snap.ts +176 -0
  357. package/src/lib/wall-placement-pointer.test.ts +23 -0
  358. package/src/lib/wall-placement-pointer.ts +18 -0
  359. package/src/lib/wall-pull.test.ts +245 -0
  360. package/src/lib/wall-pull.ts +209 -0
  361. package/src/lib/wall-push-line-up.test.ts +132 -0
  362. package/src/lib/wall-region-list.ts +17 -6
  363. package/src/lib/wall-run-snap.test.ts +188 -0
  364. package/src/lib/wall-run-snap.ts +83 -0
  365. package/src/lib/wall-type.test.ts +125 -0
  366. package/src/lib/wall-type.ts +119 -0
  367. package/src/lib/window-interaction.test.ts +44 -0
  368. package/src/lib/world-grid-snap.test.ts +139 -2
  369. package/src/lib/world-grid-snap.ts +102 -105
  370. package/src/store/use-alignment-guides.ts +27 -1
  371. package/src/store/use-editor.tsx +53 -0
  372. package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
  373. package/src/store/use-floorplan-annotation-visibility.ts +47 -22
  374. package/src/store/use-placement-typing.test.ts +165 -0
  375. package/src/store/use-placement-typing.ts +186 -0
  376. package/src/store/use-wall-snap-indicator.ts +15 -2
  377. package/src/components/tools/elevator/elevator-defaults.ts +0 -8
  378. package/src/components/tools/elevator/elevator-tool.tsx +0 -372
  379. package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
  380. package/src/lib/door-interaction.ts +0 -102
  381. package/src/lib/measurement-label.ts +0 -68
  382. package/src/lib/window-interaction.ts +0 -100
@@ -1,31 +1,25 @@
1
1
  'use client'
2
2
 
3
- import { emitter } from '@pascal-app/core'
3
+ import { emitter, type PascalI18n, usePascalI18n } from '@pascal-app/core'
4
+ import { CLAY_PALETTE, getSceneTheme, SCENE_THEMES, useViewer } from '@pascal-app/viewer'
4
5
  import {
5
- CLAY_PALETTE,
6
- type EdgeMode,
7
- getSceneTheme,
8
- SCENE_THEMES,
9
- useViewer,
10
- } from '@pascal-app/viewer'
11
- import {
12
- Box,
13
6
  Camera,
14
7
  Check,
15
8
  Contrast,
16
9
  Diamond,
10
+ Drone,
17
11
  Eye,
18
12
  EyeOff,
19
13
  Footprints,
20
14
  Layers,
21
15
  Layers2,
22
16
  Palette,
23
- PenLine,
24
17
  SlidersHorizontal,
25
- Sparkles,
26
18
  Square,
27
19
  SwatchBook,
28
20
  } from 'lucide-react'
21
+ import { useIsMobile } from '../../hooks/use-mobile'
22
+ import { sceneThemeName } from '../../i18n/scene-theme-name'
29
23
  import { cn } from '../../lib/utils'
30
24
  import { ActionButton } from '../ui/action-menu/action-button'
31
25
  import { FollowPascalToggle } from '../ui/action-menu/follow-pascal-toggle'
@@ -40,44 +34,36 @@ import {
40
34
  DropdownMenuTrigger,
41
35
  } from '../ui/primitives/dropdown-menu'
42
36
  import { TooltipProvider } from '../ui/primitives/tooltip'
43
-
44
- const levelModeLabels: Record<'stacked' | 'exploded' | 'solo', string> = {
45
- stacked: 'Stacked',
46
- exploded: 'Exploded',
47
- solo: 'Solo',
48
- }
37
+ import { DaylightMenu, EdgesToggleItem, QualityMenu } from './render-settings-menu'
38
+ import { SKY_DOCK_HEIGHT, SkyControl } from './sky-control'
49
39
 
50
40
  const wallModeConfig = {
51
41
  up: {
52
- icon: (props: any) => (
53
- <img alt="Full height" height={28} src="/icons/room.webp" width={28} {...props} />
54
- ),
55
- label: 'Full height',
42
+ icon: '/icons/room.webp',
56
43
  },
57
44
  cutaway: {
58
- icon: (props: any) => (
59
- <img alt="Cutaway" height={28} src="/icons/wallcut.webp" width={28} {...props} />
60
- ),
61
- label: 'Cutaway',
45
+ icon: '/icons/wallcut.webp',
62
46
  },
63
47
  down: {
64
- icon: (props: any) => (
65
- <img alt="Low" height={28} src="/icons/walllow.webp" width={28} {...props} />
66
- ),
67
- label: 'Low',
48
+ icon: '/icons/walllow.webp',
68
49
  },
69
- }
50
+ } as const
70
51
 
71
- const SHADING_OPTIONS = [
72
- { id: 'solid', name: 'Solid', detail: 'Flat and fast — no ambient occlusion', icon: Box },
73
- { id: 'rendered', name: 'Rendered', detail: 'Full ambient occlusion', icon: Sparkles },
74
- ] as const
52
+ function levelModeLabel(
53
+ mode: 'stacked' | 'exploded' | 'solo' | 'manual',
54
+ i18n: PascalI18n,
55
+ ): string {
56
+ if (mode === 'stacked') return i18n._({ id: 'viewer.levels.stacked', message: 'Stacked' })
57
+ if (mode === 'exploded') return i18n._({ id: 'viewer.levels.exploded', message: 'Exploded' })
58
+ if (mode === 'solo') return i18n._({ id: 'viewer.levels.solo', message: 'Solo' })
59
+ return i18n._({ id: 'viewer.levels.manual', message: 'Manual' })
60
+ }
75
61
 
76
- const EDGE_OPTIONS = [
77
- { id: 'off', name: 'Off', detail: 'No edge lines' },
78
- { id: 'soft', name: 'Soft', detail: 'Faint outline of major creases' },
79
- { id: 'strong', name: 'Strong', detail: 'Crisp, opaque edge lines' },
80
- ] as const satisfies readonly { id: EdgeMode; name: string; detail: string }[]
62
+ function wallModeLabel(mode: keyof typeof wallModeConfig, i18n: PascalI18n): string {
63
+ if (mode === 'up') return i18n._({ id: 'viewer.walls.fullHeight', message: 'Full height' })
64
+ if (mode === 'down') return i18n._({ id: 'viewer.walls.low', message: 'Low' })
65
+ return i18n._({ id: 'viewer.walls.cutaway', message: 'Cutaway' })
66
+ }
81
67
 
82
68
  // Keep the dropdown open when flipping an in-place toggle row.
83
69
  const keepOpen = (event: Event, fn: () => void) => {
@@ -95,6 +81,7 @@ function VisibilityMenu({
95
81
  canShowScans: boolean
96
82
  canShowGuides: boolean
97
83
  }) {
84
+ const i18n = usePascalI18n()
98
85
  const showScans = useViewer((s) => s.showScans)
99
86
  const showGuides = useViewer((s) => s.showGuides)
100
87
  return (
@@ -102,7 +89,7 @@ function VisibilityMenu({
102
89
  <DropdownMenuTrigger asChild>
103
90
  <ActionButton
104
91
  className="hover:bg-white/5 hover:text-foreground"
105
- label="Visibility"
92
+ label={i18n._({ id: 'viewer.visibility.title', message: 'Visibility' })}
106
93
  size="icon"
107
94
  tooltipClassName="dark"
108
95
  tooltipSide="top"
@@ -117,7 +104,7 @@ function VisibilityMenu({
117
104
  onSelect={(e) => keepOpen(e, () => useViewer.getState().setShowScans(!showScans))}
118
105
  >
119
106
  <img alt="" className="h-4 w-4 object-contain" src="/icons/mesh.webp" />
120
- <span>Scans</span>
107
+ <span>{i18n._({ id: 'viewer.visibility.scans', message: 'Scans' })}</span>
121
108
  {showScans ? (
122
109
  <Eye className="ml-auto h-4 w-4 text-foreground" />
123
110
  ) : (
@@ -130,7 +117,7 @@ function VisibilityMenu({
130
117
  onSelect={(e) => keepOpen(e, () => useViewer.getState().setShowGuides(!showGuides))}
131
118
  >
132
119
  <img alt="" className="h-4 w-4 object-contain" src="/icons/floorplan.webp" />
133
- <span>Guides</span>
120
+ <span>{i18n._({ id: 'viewer.visibility.guides', message: 'Guides' })}</span>
134
121
  {showGuides ? (
135
122
  <Eye className="ml-auto h-4 w-4 text-foreground" />
136
123
  ) : (
@@ -146,21 +133,18 @@ function VisibilityMenu({
146
133
  // One "Display" button gathering shadows, camera projection, colors, render
147
134
  // mode, scene theme and edges.
148
135
  function DisplayMenu() {
136
+ const i18n = usePascalI18n()
149
137
  const cameraMode = useViewer((s) => s.cameraMode)
150
- const shading = useViewer((s) => s.shading)
151
138
  const textures = useViewer((s) => s.textures)
152
139
  const shadows = useViewer((s) => s.shadows)
153
140
  const sceneTheme = useViewer((s) => s.sceneTheme)
154
- const edges = useViewer((s) => s.edges)
155
- const activeShading = SHADING_OPTIONS.find((o) => o.id === shading) ?? SHADING_OPTIONS[0]
156
141
  const activeTheme = getSceneTheme(sceneTheme)
157
- const activeEdges = EDGE_OPTIONS.find((o) => o.id === edges) ?? EDGE_OPTIONS[0]
158
142
  return (
159
143
  <DropdownMenu>
160
144
  <DropdownMenuTrigger asChild>
161
145
  <ActionButton
162
146
  className="hover:bg-white/5 hover:text-foreground"
163
- label="Display settings"
147
+ label={i18n._({ id: 'viewer.display.title', message: 'Display settings' })}
164
148
  size="icon"
165
149
  tooltipClassName="dark"
166
150
  tooltipSide="top"
@@ -174,8 +158,12 @@ function DisplayMenu() {
174
158
  onSelect={(e) => keepOpen(e, () => useViewer.getState().setShadows(!shadows))}
175
159
  >
176
160
  <Contrast className="h-4 w-4" />
177
- <span>Shadows</span>
178
- <span className="ml-auto text-muted-foreground text-xs">{shadows ? 'On' : 'Off'}</span>
161
+ <span>{i18n._({ id: 'viewer.display.shadows', message: 'Shadows' })}</span>
162
+ <span className="ml-auto text-muted-foreground text-xs">
163
+ {shadows
164
+ ? i18n._({ id: 'viewer.common.on', message: 'On' })
165
+ : i18n._({ id: 'viewer.common.off', message: 'Off' })}
166
+ </span>
179
167
  </DropdownMenuItem>
180
168
  <DropdownMenuItem
181
169
  onSelect={(e) =>
@@ -187,107 +175,70 @@ function DisplayMenu() {
187
175
  }
188
176
  >
189
177
  <Camera className="h-4 w-4" />
190
- <span>Camera</span>
178
+ <span>{i18n._({ id: 'viewer.display.camera', message: 'Camera' })}</span>
191
179
  <span className="ml-auto text-muted-foreground text-xs">
192
- {cameraMode === 'perspective' ? 'Perspective' : 'Orthographic'}
180
+ {cameraMode === 'perspective'
181
+ ? i18n._({ id: 'viewer.display.perspective', message: 'Perspective' })
182
+ : i18n._({ id: 'viewer.display.orthographic', message: 'Orthographic' })}
193
183
  </span>
194
184
  </DropdownMenuItem>
195
185
  <DropdownMenuItem
196
186
  onSelect={(e) => keepOpen(e, () => useViewer.getState().setTextures(!textures))}
197
187
  >
198
188
  {textures ? <Palette className="h-4 w-4" /> : <Square className="h-4 w-4" />}
199
- <span>Colors</span>
189
+ <span>{i18n._({ id: 'viewer.display.colors', message: 'Colors' })}</span>
200
190
  <span className="ml-auto text-muted-foreground text-xs">
201
- {textures ? 'Colored' : 'Monochrome'}
191
+ {textures
192
+ ? i18n._({ id: 'viewer.display.colored', message: 'Colored' })
193
+ : i18n._({ id: 'viewer.display.monochrome', message: 'Monochrome' })}
202
194
  </span>
203
195
  </DropdownMenuItem>
204
196
 
205
197
  <DropdownMenuSeparator />
206
198
 
207
- <DropdownMenuSub>
208
- <DropdownMenuSubTrigger>
209
- <activeShading.icon className="h-4 w-4" />
210
- <span>Render</span>
211
- <span className="ml-auto text-muted-foreground text-xs">{activeShading.name}</span>
212
- </DropdownMenuSubTrigger>
213
- <DropdownMenuSubContent className="dark min-w-56">
214
- {SHADING_OPTIONS.map((option) => {
215
- const OptionIcon = option.icon
216
- return (
217
- <DropdownMenuItem
218
- key={option.id}
219
- onSelect={() => useViewer.getState().setShading(option.id)}
220
- >
221
- <OptionIcon className="h-4 w-4" />
222
- <div className="flex flex-col">
223
- <span className="text-foreground">{option.name}</span>
224
- <span className="text-muted-foreground text-xs">{option.detail}</span>
225
- </div>
226
- {shading === option.id ? (
227
- <Check className="ml-auto h-4 w-4 text-foreground" />
228
- ) : null}
229
- </DropdownMenuItem>
230
- )
231
- })}
232
- </DropdownMenuSubContent>
233
- </DropdownMenuSub>
199
+ <QualityMenu subContentClassName="dark min-w-56" />
234
200
 
235
- <DropdownMenuSub>
236
- <DropdownMenuSubTrigger>
237
- <SwatchBook className="h-4 w-4" />
238
- <span>Scene theme</span>
239
- <span className="ml-auto truncate text-muted-foreground text-xs">
240
- {activeTheme.name}
241
- </span>
242
- </DropdownMenuSubTrigger>
243
- <DropdownMenuSubContent className="dark min-w-48">
244
- {SCENE_THEMES.map((t) => {
245
- const swatches = (['wall', 'roof', 'floor', 'glazing'] as const).map(
246
- (role) => t.clayTints?.[role] ?? CLAY_PALETTE[role],
247
- )
248
- return (
249
- <DropdownMenuItem
250
- className="gap-2"
251
- key={t.id}
252
- onSelect={() => useViewer.getState().setSceneTheme(t.id)}
253
- >
254
- <span
255
- className="grid h-5 w-5 shrink-0 grid-cols-2 overflow-hidden rounded-sm border border-black/10"
256
- style={{ backgroundColor: t.background }}
257
- >
258
- {swatches.map((color, index) => (
259
- <span key={`${t.id}-${index}`} style={{ backgroundColor: color }} />
260
- ))}
261
- </span>
262
- <span>{t.name}</span>
263
- {sceneTheme === t.id ? <Check className="ml-auto h-4 w-4" /> : null}
264
- </DropdownMenuItem>
265
- )
266
- })}
267
- </DropdownMenuSubContent>
268
- </DropdownMenuSub>
269
-
270
- <DropdownMenuSub>
271
- <DropdownMenuSubTrigger>
272
- <PenLine className="h-4 w-4" />
273
- <span>Edges</span>
274
- <span className="ml-auto text-muted-foreground text-xs">{activeEdges.name}</span>
275
- </DropdownMenuSubTrigger>
276
- <DropdownMenuSubContent className="dark min-w-56">
277
- {EDGE_OPTIONS.map((option) => (
278
- <DropdownMenuItem
279
- key={option.id}
280
- onSelect={() => useViewer.getState().setEdges(option.id)}
281
- >
282
- <div className="flex flex-col">
283
- <span className="text-foreground">{option.name}</span>
284
- <span className="text-muted-foreground text-xs">{option.detail}</span>
285
- </div>
286
- {edges === option.id ? <Check className="ml-auto h-4 w-4 text-foreground" /> : null}
287
- </DropdownMenuItem>
288
- ))}
289
- </DropdownMenuSubContent>
290
- </DropdownMenuSub>
201
+ {textures ? (
202
+ <DaylightMenu subContentClassName="dark min-w-48" />
203
+ ) : (
204
+ <>
205
+ <DropdownMenuSub>
206
+ <DropdownMenuSubTrigger>
207
+ <SwatchBook className="h-4 w-4" />
208
+ <span>{i18n._({ id: 'viewer.theme.title', message: 'Scene theme' })}</span>
209
+ <span className="ml-auto truncate text-muted-foreground text-xs">
210
+ {sceneThemeName(activeTheme.id, activeTheme.name, i18n)}
211
+ </span>
212
+ </DropdownMenuSubTrigger>
213
+ <DropdownMenuSubContent className="dark min-w-48">
214
+ {SCENE_THEMES.map((theme) => {
215
+ const swatches = (['wall', 'roof', 'floor', 'glazing'] as const).map(
216
+ (role) => theme.clayTints?.[role] ?? CLAY_PALETTE[role],
217
+ )
218
+ return (
219
+ <DropdownMenuItem
220
+ className="gap-2"
221
+ key={theme.id}
222
+ onSelect={() => useViewer.getState().setSceneTheme(theme.id)}
223
+ >
224
+ <span
225
+ className="grid h-5 w-5 shrink-0 grid-cols-2 overflow-hidden rounded-sm border border-black/10"
226
+ style={{ backgroundColor: theme.background }}
227
+ >
228
+ {swatches.map((color, index) => (
229
+ <span key={`${theme.id}-${index}`} style={{ backgroundColor: color }} />
230
+ ))}
231
+ </span>
232
+ <span>{sceneThemeName(theme.id, theme.name, i18n)}</span>
233
+ {sceneTheme === theme.id ? <Check className="ml-auto h-4 w-4" /> : null}
234
+ </DropdownMenuItem>
235
+ )
236
+ })}
237
+ </DropdownMenuSubContent>
238
+ </DropdownMenuSub>
239
+ <EdgesToggleItem />
240
+ </>
241
+ )}
291
242
  </DropdownMenuContent>
292
243
  </DropdownMenu>
293
244
  )
@@ -306,6 +257,9 @@ export type ViewerControlsBarProps = {
306
257
  glbHasGuides?: boolean
307
258
  walkthroughActive?: boolean
308
259
  onWalkthroughToggle: () => void
260
+ /** The free-flying drone camera; its button shows only with a handler, and never on touch. */
261
+ droneActive?: boolean
262
+ onDroneToggle?: () => void
309
263
  className?: string
310
264
  }
311
265
 
@@ -317,162 +271,226 @@ export const ViewerControlsBar = ({
317
271
  glbHasGuides = false,
318
272
  walkthroughActive = false,
319
273
  onWalkthroughToggle,
274
+ droneActive = false,
275
+ onDroneToggle,
320
276
  className,
321
277
  }: ViewerControlsBarProps) => {
278
+ const i18n = usePascalI18n()
322
279
  const levelMode = useViewer((s) => s.levelMode)
323
280
  const wallMode = useViewer((s) => s.wallMode)
281
+ const textures = useViewer((s) => s.textures)
282
+ const isMobile = useIsMobile()
324
283
  // Sessions may carry a stale mode outside the cycle (e.g. the retired
325
284
  // 'translucent'); render and cycle it as cutaway instead of crashing.
326
285
  const safeWallMode = (
327
286
  wallMode in wallModeConfig ? wallMode : 'cutaway'
328
287
  ) as keyof typeof wallModeConfig
329
- const WallModeIcon = wallModeConfig[safeWallMode].icon
330
288
 
331
289
  return (
332
- <div
333
- className={cn(
334
- 'dark absolute bottom-4 left-1/2 z-20 -translate-x-1/2 text-foreground sm:bottom-6',
335
- className,
336
- )}
337
- >
338
- <TooltipProvider delayDuration={0}>
339
- <div className="corner-smooth pointer-events-auto flex h-12 max-w-[calc(100vw-1rem)] flex-row items-center justify-center gap-0.5 overflow-hidden rounded-2xl border border-border/40 bg-background/95 p-1 shadow-elevation-4 backdrop-blur-xl transition-colors duration-200 ease-out sm:h-14 sm:gap-1.5 sm:p-1.5">
340
- {((canShowScans && (!glbActive || glbHasScans)) ||
341
- (canShowGuides && (!glbActive || glbHasGuides))) && (
342
- <>
343
- <VisibilityMenu
344
- canShowGuides={canShowGuides && (!glbActive || glbHasGuides)}
345
- canShowScans={canShowScans && (!glbActive || glbHasScans)}
346
- />
347
- <div className="mx-1 h-5 w-px bg-border/40" />
348
- </>
349
- )}
350
-
351
- {/* Level mode */}
352
- <ActionButton
353
- className={
354
- levelMode === 'stacked'
355
- ? 'hover:bg-white/5 hover:text-amber-400'
356
- : 'bg-amber-500/20 text-amber-400'
357
- }
358
- label={`Levels: ${levelMode === 'manual' ? 'Manual' : levelModeLabels[levelMode as keyof typeof levelModeLabels]}`}
359
- onClick={() => {
360
- if (levelMode === 'manual') return useViewer.getState().setLevelMode('stacked')
361
- const modes: ('stacked' | 'exploded' | 'solo')[] = ['stacked', 'exploded', 'solo']
362
- const nextIndex = (modes.indexOf(levelMode as any) + 1) % modes.length
363
- useViewer.getState().setLevelMode(modes[nextIndex] ?? 'stacked')
364
- }}
365
- size="icon"
366
- tooltipClassName="dark"
367
- tooltipSide="top"
368
- variant="ghost"
290
+ <>
291
+ {textures && !isMobile ? (
292
+ // The sky (time and weather) beside the compass in the scene's bottom-left corner.
293
+ <SkyControl dark height={SKY_DOCK_HEIGHT.viewer} style={{ left: 52 }} />
294
+ ) : null}
295
+ <div
296
+ className={cn(
297
+ 'dark absolute bottom-4 left-1/2 z-20 -translate-x-1/2 text-foreground sm:bottom-6',
298
+ className,
299
+ )}
300
+ >
301
+ <TooltipProvider delayDuration={0}>
302
+ <div
303
+ data-surface="viewer-dock"
304
+ className="corner-smooth pointer-events-auto flex h-12 max-w-[calc(100vw-1rem)] flex-row items-center justify-center gap-0.5 overflow-hidden rounded-2xl border border-border/40 bg-background/95 p-1 shadow-elevation-4 backdrop-blur-xl transition-colors duration-200 ease-out sm:h-14 sm:gap-1.5 sm:p-1.5"
369
305
  >
370
- {levelMode === 'solo' && <Diamond className="h-6 w-6" />}
371
- {levelMode === 'exploded' && <Layers2 className="h-6 w-6" />}
372
- {(levelMode === 'stacked' || levelMode === 'manual') && <Layers className="h-6 w-6" />}
373
- </ActionButton>
306
+ {((canShowScans && (!glbActive || glbHasScans)) ||
307
+ (canShowGuides && (!glbActive || glbHasGuides))) && (
308
+ <>
309
+ <VisibilityMenu
310
+ canShowGuides={canShowGuides && (!glbActive || glbHasGuides)}
311
+ canShowScans={canShowScans && (!glbActive || glbHasScans)}
312
+ />
313
+ <div className="mx-1 h-5 w-px bg-border/40" />
314
+ </>
315
+ )}
374
316
 
375
- {/* Wall mode — parametric only; baked GLB walls are fixed-height. */}
376
- {!glbActive && (
317
+ {/* Level mode */}
377
318
  <ActionButton
378
319
  className={
379
- safeWallMode === 'cutaway'
380
- ? 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0'
381
- : 'bg-white/10'
320
+ levelMode === 'stacked'
321
+ ? 'hover:bg-white/5 hover:text-amber-400'
322
+ : 'bg-amber-500/20 text-amber-400'
382
323
  }
383
- label={`Walls: ${wallModeConfig[safeWallMode].label}`}
324
+ label={i18n._({
325
+ id: 'viewer.levels.label',
326
+ message: 'Levels: {mode}',
327
+ values: { mode: levelModeLabel(levelMode, i18n) },
328
+ })}
384
329
  onClick={() => {
385
- const modes: ('cutaway' | 'up' | 'down')[] = ['cutaway', 'up', 'down']
386
- const nextIndex = (modes.indexOf(safeWallMode) + 1) % modes.length
387
- useViewer.getState().setWallMode(modes[nextIndex] ?? 'cutaway')
330
+ if (levelMode === 'manual') return useViewer.getState().setLevelMode('stacked')
331
+ const modes: ('stacked' | 'exploded' | 'solo')[] = ['stacked', 'exploded', 'solo']
332
+ const nextIndex = (modes.indexOf(levelMode as any) + 1) % modes.length
333
+ useViewer.getState().setLevelMode(modes[nextIndex] ?? 'stacked')
388
334
  }}
389
335
  size="icon"
390
336
  tooltipClassName="dark"
391
337
  tooltipSide="top"
392
338
  variant="ghost"
393
339
  >
394
- <WallModeIcon className="h-[28px] w-[28px]" />
340
+ {levelMode === 'solo' && <Diamond className="h-6 w-6" />}
341
+ {levelMode === 'exploded' && <Layers2 className="h-6 w-6" />}
342
+ {(levelMode === 'stacked' || levelMode === 'manual') && (
343
+ <Layers className="h-6 w-6" />
344
+ )}
395
345
  </ActionButton>
396
- )}
397
346
 
398
- <div className="mx-1 h-5 w-px bg-border/40" />
347
+ {/* Wall mode — parametric only; baked GLB walls are fixed-height. */}
348
+ {!glbActive && (
349
+ <ActionButton
350
+ className={
351
+ safeWallMode === 'cutaway'
352
+ ? 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0'
353
+ : 'bg-white/10'
354
+ }
355
+ label={i18n._({
356
+ id: 'viewer.walls.label',
357
+ message: 'Walls: {mode}',
358
+ values: { mode: wallModeLabel(safeWallMode, i18n) },
359
+ })}
360
+ onClick={() => {
361
+ const modes: ('cutaway' | 'up' | 'down')[] = ['cutaway', 'up', 'down']
362
+ const nextIndex = (modes.indexOf(safeWallMode) + 1) % modes.length
363
+ useViewer.getState().setWallMode(modes[nextIndex] ?? 'cutaway')
364
+ }}
365
+ size="icon"
366
+ tooltipClassName="dark"
367
+ tooltipSide="top"
368
+ variant="ghost"
369
+ >
370
+ <img
371
+ alt={wallModeLabel(safeWallMode, i18n)}
372
+ className="h-[28px] w-[28px]"
373
+ height={28}
374
+ src={wallModeConfig[safeWallMode].icon}
375
+ width={28}
376
+ />
377
+ </ActionButton>
378
+ )}
399
379
 
400
- <DisplayMenu />
380
+ <div className="mx-1 h-5 w-px bg-border/40" />
401
381
 
402
- <div className="mx-1 h-5 w-px bg-border/40" />
382
+ <DisplayMenu />
403
383
 
404
- {/* Walkthrough */}
405
- <ActionButton
406
- className={
407
- walkthroughActive
408
- ? 'bg-emerald-500/20 text-emerald-400'
409
- : 'hover:bg-white/5 hover:text-emerald-400'
410
- }
411
- label={`Walkthrough: ${walkthroughActive ? 'On' : 'Off'}`}
412
- onClick={onWalkthroughToggle}
413
- size="icon"
414
- tooltipClassName="dark"
415
- tooltipSide="top"
416
- variant="ghost"
417
- >
418
- <Footprints className="h-6 w-6" />
419
- </ActionButton>
384
+ <div className="mx-1 h-5 w-px bg-border/40" />
420
385
 
421
- <div className="mx-1 h-5 w-px bg-border/40" />
386
+ {/* Walkthrough */}
387
+ <ActionButton
388
+ className={
389
+ walkthroughActive
390
+ ? 'bg-emerald-500/20 text-emerald-400'
391
+ : 'hover:bg-white/5 hover:text-emerald-400'
392
+ }
393
+ label={i18n._({
394
+ id: 'viewer.walkthrough.label',
395
+ message: 'Walkthrough: {state}',
396
+ values: {
397
+ state: walkthroughActive
398
+ ? i18n._({ id: 'viewer.common.on', message: 'On' })
399
+ : i18n._({ id: 'viewer.common.off', message: 'Off' }),
400
+ },
401
+ })}
402
+ onClick={onWalkthroughToggle}
403
+ size="icon"
404
+ tooltipClassName="dark"
405
+ tooltipSide="top"
406
+ variant="ghost"
407
+ >
408
+ <Footprints className="h-6 w-6" />
409
+ </ActionButton>
422
410
 
423
- {/* Follow Pascal */}
424
- <FollowPascalToggle className="hidden sm:inline-flex" tooltipClassName="dark" />
411
+ {/* Drone — flying needs a keyboard. */}
412
+ {onDroneToggle && !isMobile && (
413
+ <ActionButton
414
+ className={
415
+ droneActive
416
+ ? 'bg-emerald-500/20 text-emerald-400'
417
+ : 'hover:bg-white/5 hover:text-emerald-400'
418
+ }
419
+ label={i18n._({
420
+ id: 'viewer.drone.label',
421
+ message: 'Drone: {state}',
422
+ values: {
423
+ state: droneActive
424
+ ? i18n._({ id: 'viewer.common.on', message: 'On' })
425
+ : i18n._({ id: 'viewer.common.off', message: 'Off' }),
426
+ },
427
+ })}
428
+ onClick={onDroneToggle}
429
+ size="icon"
430
+ tooltipClassName="dark"
431
+ tooltipSide="top"
432
+ variant="ghost"
433
+ >
434
+ <Drone className="h-6 w-6" />
435
+ </ActionButton>
436
+ )}
425
437
 
426
- {/* Camera actions */}
427
- <ActionButton
428
- className="group hidden hover:bg-white/5 sm:inline-flex"
429
- label="Orbit left"
430
- onClick={() => emitter.emit('camera-controls:orbit-ccw')}
431
- size="icon"
432
- tooltipClassName="dark"
433
- tooltipSide="top"
434
- variant="ghost"
435
- >
436
- <img
437
- alt="Orbit left"
438
- className="h-[28px] w-[28px] -scale-x-100 object-contain opacity-70 transition-opacity group-hover:opacity-100"
439
- src="/icons/rotate.webp"
440
- />
441
- </ActionButton>
438
+ <div className="mx-1 h-5 w-px bg-border/40" />
442
439
 
443
- <ActionButton
444
- className="group hidden hover:bg-white/5 sm:inline-flex"
445
- label="Orbit right"
446
- onClick={() => emitter.emit('camera-controls:orbit-cw')}
447
- size="icon"
448
- tooltipClassName="dark"
449
- tooltipSide="top"
450
- variant="ghost"
451
- >
452
- <img
453
- alt="Orbit right"
454
- className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
455
- src="/icons/rotate.webp"
456
- />
457
- </ActionButton>
440
+ {/* Follow Pascal */}
441
+ <FollowPascalToggle className="hidden sm:inline-flex" tooltipClassName="dark" />
458
442
 
459
- <ActionButton
460
- className="group hover:bg-white/5"
461
- label="Top view"
462
- onClick={() => emitter.emit('camera-controls:top-view')}
463
- size="icon"
464
- tooltipClassName="dark"
465
- tooltipSide="top"
466
- variant="ghost"
467
- >
468
- <img
469
- alt="Top view"
470
- className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
471
- src="/icons/topview.webp"
472
- />
473
- </ActionButton>
474
- </div>
475
- </TooltipProvider>
476
- </div>
443
+ {/* Camera actions */}
444
+ <ActionButton
445
+ className="group hidden hover:bg-white/5 sm:inline-flex"
446
+ label={i18n._({ id: 'viewer.camera.orbitLeft', message: 'Orbit left' })}
447
+ onClick={() => emitter.emit('camera-controls:orbit-ccw')}
448
+ size="icon"
449
+ tooltipClassName="dark"
450
+ tooltipSide="top"
451
+ variant="ghost"
452
+ >
453
+ <img
454
+ alt={i18n._({ id: 'viewer.camera.orbitLeft', message: 'Orbit left' })}
455
+ className="h-[28px] w-[28px] -scale-x-100 object-contain opacity-70 transition-opacity group-hover:opacity-100"
456
+ src="/icons/rotate.webp"
457
+ />
458
+ </ActionButton>
459
+
460
+ <ActionButton
461
+ className="group hidden hover:bg-white/5 sm:inline-flex"
462
+ label={i18n._({ id: 'viewer.camera.orbitRight', message: 'Orbit right' })}
463
+ onClick={() => emitter.emit('camera-controls:orbit-cw')}
464
+ size="icon"
465
+ tooltipClassName="dark"
466
+ tooltipSide="top"
467
+ variant="ghost"
468
+ >
469
+ <img
470
+ alt={i18n._({ id: 'viewer.camera.orbitRight', message: 'Orbit right' })}
471
+ className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
472
+ src="/icons/rotate.webp"
473
+ />
474
+ </ActionButton>
475
+
476
+ <ActionButton
477
+ className="group hover:bg-white/5"
478
+ label={i18n._({ id: 'viewer.camera.topView', message: 'Top view' })}
479
+ onClick={() => emitter.emit('camera-controls:top-view')}
480
+ size="icon"
481
+ tooltipClassName="dark"
482
+ tooltipSide="top"
483
+ variant="ghost"
484
+ >
485
+ <img
486
+ alt={i18n._({ id: 'viewer.camera.topView', message: 'Top view' })}
487
+ className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
488
+ src="/icons/topview.webp"
489
+ />
490
+ </ActionButton>
491
+ </div>
492
+ </TooltipProvider>
493
+ </div>
494
+ </>
477
495
  )
478
496
  }