@pascal-app/editor 1.1.0 → 1.1.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 (260) hide show
  1. package/package.json +9 -15
  2. package/src/components/editor/action-menu-button.tsx +1 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
  4. package/src/components/editor/custom-camera-controls.tsx +11 -11
  5. package/src/components/editor/editor-layout-v2.tsx +1 -1
  6. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  7. package/src/components/editor/first-person-controls.tsx +20 -1981
  8. package/src/components/editor/floating-action-menu.tsx +1 -1
  9. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  10. package/src/components/editor/floor-region-controls.tsx +6 -2
  11. package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
  12. package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
  13. package/src/components/editor/floorplan-panel.tsx +26 -22
  14. package/src/components/editor/floorplan-wall-typing-keys.ts +1 -1
  15. package/src/components/editor/follow-pascal.test.tsx +1 -2
  16. package/src/components/editor/follow-pascal.tsx +1 -1
  17. package/src/components/editor/footprint-height-handle.tsx +1 -1
  18. package/src/components/editor/group-actions.ts +1 -2
  19. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  20. package/src/components/editor/group-move-3d.ts +1 -2
  21. package/src/components/editor/group-rotate-handle.tsx +1 -2
  22. package/src/components/editor/handles/use-handle-drag.ts +1 -2
  23. package/src/components/editor/index.tsx +24 -14
  24. package/src/components/editor/inspector-extension-actions.tsx +1 -1
  25. package/src/components/editor/node-arrow-handles.tsx +1 -1
  26. package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
  27. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  28. package/src/components/editor/room-controls.tsx +7 -4
  29. package/src/components/editor/room-handles-2d.tsx +2 -3
  30. package/src/components/editor/room-handles.tsx +1 -2
  31. package/src/components/editor/room-highlight.tsx +6 -105
  32. package/src/components/editor/room-labels.test.ts +8 -822
  33. package/src/components/editor/room-labels.tsx +40 -1671
  34. package/src/components/editor/room-webgpu-materials.test.ts +1 -1
  35. package/src/components/editor/selection-manager.tsx +5 -3
  36. package/src/components/editor/site-edge-labels.tsx +1 -1
  37. package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
  38. package/src/components/editor/use-floorplan-background-placement.ts +2 -1
  39. package/src/components/editor/views/plugins-view.tsx +1 -1
  40. package/src/components/editor/views/view-bar.tsx +9 -9
  41. package/src/components/editor/wall-measurement-label.tsx +1 -1
  42. package/src/components/editor/wall-move-side-handles.tsx +1 -1
  43. package/src/components/editor/wall-pull-grip.tsx +2 -2
  44. package/src/components/editor-2d/floorplan-group-move.tsx +2 -3
  45. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1 -1
  46. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -1
  47. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +1 -2
  48. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
  49. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
  50. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
  51. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +1 -2
  52. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +8 -10
  53. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +25 -33
  54. package/src/components/systems/roof/roof-edit-system.tsx +4 -3
  55. package/src/components/tools/distribution/distribution-run-tool.tsx +1 -1
  56. package/src/components/tools/item/placement-history.test.tsx +1 -1
  57. package/src/components/tools/item/use-placement-coordinator.tsx +2 -3
  58. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +6 -2
  59. package/src/components/tools/registry/move-registry-node-tool.tsx +1 -2
  60. package/src/components/tools/shared/cursor-sphere.tsx +1 -1
  61. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  62. package/src/components/tools/shared/placement-box.tsx +1 -1
  63. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  64. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  65. package/src/components/ui/action-menu/camera-actions.tsx +2 -2
  66. package/src/components/ui/action-menu/control-modes.tsx +1 -1
  67. package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
  68. package/src/components/ui/action-menu/index.tsx +3 -3
  69. package/src/components/ui/action-menu/measurement-control.tsx +3 -3
  70. package/src/components/ui/action-menu/placement.ts +0 -18
  71. package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
  72. package/src/components/ui/action-menu/view-toggles.tsx +5 -5
  73. package/src/components/ui/controls/finish-swatch.tsx +1 -1
  74. package/src/components/ui/controls/material-paint-panel.tsx +2 -2
  75. package/src/components/ui/controls/scene-material-list.tsx +2 -2
  76. package/src/components/ui/controls/select-control.tsx +1 -1
  77. package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
  78. package/src/components/ui/floating-level-selector.tsx +3 -3
  79. package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
  80. package/src/components/ui/helpers/helper-manager.test.tsx +1 -1
  81. package/src/components/ui/helpers/helper-manager.tsx +1 -1
  82. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  83. package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
  84. package/src/components/ui/panels/floor-foundation-panel.tsx +2 -2
  85. package/src/components/ui/panels/floor-opening-panel.tsx +4 -4
  86. package/src/components/ui/panels/node-display.ts +1 -1
  87. package/src/components/ui/panels/panel-manager.tsx +1 -1
  88. package/src/components/ui/panels/panel-wrapper.tsx +1 -1
  89. package/src/components/ui/panels/parametric-inspector.tsx +1 -1
  90. package/src/components/ui/panels/room-construction-rows.tsx +6 -6
  91. package/src/components/ui/panels/room-name-options.ts +1 -5
  92. package/src/components/ui/panels/room-panel.tsx +6 -8
  93. package/src/components/ui/primitives/color-dot.tsx +2 -2
  94. package/src/components/ui/primitives/opacity-control.tsx +2 -2
  95. package/src/components/ui/primitives/sidebar.tsx +3 -3
  96. package/src/components/ui/right-stack.tsx +1 -1
  97. package/src/components/ui/sidebar/icon-rail.tsx +1 -1
  98. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  99. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  100. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
  101. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
  102. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
  103. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -1
  104. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  105. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
  106. package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -2
  107. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
  108. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  109. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
  110. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
  111. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
  112. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
  113. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
  114. package/src/components/ui/sidebar/tab-bar.tsx +1 -1
  115. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
  116. package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
  117. package/src/components/viewer-overlay.tsx +1 -2
  118. package/src/hooks/use-auto-frame.ts +1 -2
  119. package/src/hooks/use-keyboard.ts +1 -2
  120. package/src/hooks/use-selected-room.ts +5 -7
  121. package/src/i18n/floor-footprint-name.ts +1 -1
  122. package/src/i18n/locales/en/messages.po +180 -182
  123. package/src/i18n/locales/es/messages.po +180 -182
  124. package/src/i18n/locales/fr/messages.po +180 -182
  125. package/src/i18n/locales/ja/messages.po +180 -182
  126. package/src/i18n/locales/zh-CN/messages.po +180 -182
  127. package/src/i18n/locales.test.ts +1 -1
  128. package/src/index.tsx +109 -131
  129. package/src/lib/below-grade.test.tsx +4 -4
  130. package/src/lib/camera-facing.test.ts +1 -1
  131. package/src/lib/camera-facing.ts +2 -3
  132. package/src/lib/editor-level-display.ts +1 -1
  133. package/src/lib/floor-footprints.test.ts +2 -2
  134. package/src/lib/floor-opening-draft.ts +1 -1
  135. package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
  136. package/src/lib/floorplan/annotation-visibility.ts +1 -1
  137. package/src/lib/floorplan/floorplan-export.test.ts +1 -1
  138. package/src/lib/floorplan/floorplan-export.tsx +8 -11
  139. package/src/lib/floorplan/floorplan-extension.ts +4 -157
  140. package/src/lib/floorplan/floorplan-mode.ts +2 -1
  141. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
  142. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
  143. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
  144. package/src/lib/floorplan/geometry.ts +2 -112
  145. package/src/lib/floorplan/index.ts +0 -9
  146. package/src/lib/floorplan/items.ts +2 -1
  147. package/src/lib/floorplan/schedules.ts +6 -3
  148. package/src/lib/floorplan/sheet-export.ts +7 -3
  149. package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
  150. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
  151. package/src/lib/follow-pascal-presentation.ts +1 -1
  152. package/src/lib/glb-export.ts +1 -1
  153. package/src/lib/material-paint.ts +1 -1
  154. package/src/lib/measurements.test.ts +2 -1
  155. package/src/lib/mezzanine-selection.test.ts +1 -1
  156. package/src/lib/paint-region-mode.ts +1 -1
  157. package/src/lib/point-ask/session.ts +1 -2
  158. package/src/lib/roof-duplication.test.ts +100 -0
  159. package/src/lib/roof-duplication.ts +30 -5
  160. package/src/lib/room-handle-drag.ts +2 -12
  161. package/src/lib/room-name-catalog.test.ts +2 -1
  162. package/src/lib/room-name-messages.test.ts +2 -1
  163. package/src/lib/room-name-messages.ts +1 -118
  164. package/src/lib/room-name-storage.test.ts +2 -1
  165. package/src/lib/room-selection-commands.ts +2 -2
  166. package/src/lib/room-selection.test.ts +6 -6
  167. package/src/lib/room-transform-session.ts +1 -2
  168. package/src/lib/room-zone-routing.ts +2 -2
  169. package/src/lib/selection-routing.test.ts +0 -28
  170. package/src/lib/selection-routing.ts +1 -40
  171. package/src/lib/sfx-bus.ts +8 -32
  172. package/src/lib/sfx-player.test.ts +72 -1
  173. package/src/lib/sfx-player.ts +49 -337
  174. package/src/lib/viewer-selection.test.ts +38 -10
  175. package/src/lib/wall-pull.test.ts +1 -1
  176. package/src/lib/wall-pull.ts +1 -27
  177. package/src/store/use-editor.tsx +17 -26
  178. package/src/store/use-floorplan-annotation-visibility.ts +5 -5
  179. package/src/store/use-wall-region-handles-selection.ts +1 -2
  180. package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
  181. package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
  182. package/src/components/editor/first-person/build-collider-world.ts +0 -549
  183. package/src/components/editor/first-person/ground-holes.test.ts +0 -128
  184. package/src/components/editor/first-person/ground-holes.ts +0 -130
  185. package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
  186. package/src/components/editor/first-person/mechanism-targets.ts +0 -88
  187. package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
  188. package/src/components/editor/first-person/terrain-collider.ts +0 -212
  189. package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
  190. package/src/components/editor/floorplan-camera-sync.ts +0 -213
  191. package/src/components/editor/localized-html.tsx +0 -22
  192. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
  193. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
  194. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
  195. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
  196. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
  197. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
  198. package/src/components/editor-2d/svg-paths.ts +0 -124
  199. package/src/components/ui/action-menu/action-button.tsx +0 -89
  200. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
  201. package/src/components/ui/primitives/button.tsx +0 -69
  202. package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
  203. package/src/components/ui/primitives/popover.tsx +0 -42
  204. package/src/components/ui/primitives/switch.tsx +0 -29
  205. package/src/components/ui/primitives/tooltip.tsx +0 -57
  206. package/src/components/ui/scene-loader.tsx +0 -73
  207. package/src/components/viewer/floorplan-compass-button.tsx +0 -67
  208. package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
  209. package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
  210. package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
  211. package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
  212. package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
  213. package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
  214. package/src/components/viewer/floorplan-preview.tsx +0 -1065
  215. package/src/components/viewer/render-settings-menu.tsx +0 -294
  216. package/src/components/viewer/sky-control.tsx +0 -710
  217. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
  218. package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
  219. package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
  220. package/src/components/viewer/viewer-controls-bar.tsx +0 -496
  221. package/src/components/viewer/viewer-hover-label.tsx +0 -42
  222. package/src/components/viewer/viewer-rooms.tsx +0 -248
  223. package/src/components/viewer/viewer-scene-header.tsx +0 -286
  224. package/src/components/viewer/viewer-selection-manager.tsx +0 -124
  225. package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
  226. package/src/components/viewer/viewer-stage-modes.ts +0 -49
  227. package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
  228. package/src/components/viewer/viewer-stage.test.tsx +0 -47
  229. package/src/components/viewer/viewer-stage.tsx +0 -221
  230. package/src/components/walkthrough-hud.tsx +0 -193
  231. package/src/hooks/use-mobile.ts +0 -19
  232. package/src/i18n/level-name.ts +0 -39
  233. package/src/i18n/scene-theme-name.ts +0 -16
  234. package/src/lib/below-grade.ts +0 -133
  235. package/src/lib/camera-pose.test.ts +0 -237
  236. package/src/lib/camera-pose.ts +0 -172
  237. package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
  238. package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
  239. package/src/lib/floorplan/floorplan-readonly.ts +0 -95
  240. package/src/lib/floorplan/marks.test.ts +0 -213
  241. package/src/lib/floorplan/marks.ts +0 -238
  242. package/src/lib/measurements.ts +0 -8
  243. package/src/lib/node-display-label.ts +0 -37
  244. package/src/lib/room-assembly-overlay.test.ts +0 -275
  245. package/src/lib/room-assembly-overlay.ts +0 -265
  246. package/src/lib/room-selection.ts +0 -417
  247. package/src/lib/scene-bounds.test.ts +0 -327
  248. package/src/lib/scene-bounds.ts +0 -233
  249. package/src/lib/typing-target.ts +0 -9
  250. package/src/lib/viewer-selection.ts +0 -595
  251. package/src/lib/zone-content.ts +0 -133
  252. package/src/store/camera-pose-store.test.ts +0 -29
  253. package/src/store/camera-pose-store.ts +0 -27
  254. package/src/store/navigation-sync-pose-store.test.ts +0 -34
  255. package/src/store/navigation-sync-pose-store.ts +0 -27
  256. package/src/store/use-audio.test.ts +0 -146
  257. package/src/store/use-audio.tsx +0 -169
  258. package/src/store/use-first-person-hud.ts +0 -40
  259. package/src/store/use-follow-pascal.test.ts +0 -62
  260. package/src/store/use-follow-pascal.ts +0 -53
@@ -1,496 +0,0 @@
1
- 'use client'
2
-
3
- import { emitter, type PascalI18n, usePascalI18n } from '@pascal-app/core'
4
- import { CLAY_PALETTE, getSceneTheme, SCENE_THEMES, useViewer } from '@pascal-app/viewer'
5
- import {
6
- Camera,
7
- Check,
8
- Contrast,
9
- Diamond,
10
- Drone,
11
- Eye,
12
- EyeOff,
13
- Footprints,
14
- Layers,
15
- Layers2,
16
- Palette,
17
- SlidersHorizontal,
18
- Square,
19
- SwatchBook,
20
- } from 'lucide-react'
21
- import { useIsMobile } from '../../hooks/use-mobile'
22
- import { sceneThemeName } from '../../i18n/scene-theme-name'
23
- import { cn } from '../../lib/utils'
24
- import { ActionButton } from '../ui/action-menu/action-button'
25
- import { FollowPascalToggle } from '../ui/action-menu/follow-pascal-toggle'
26
- import {
27
- DropdownMenu,
28
- DropdownMenuContent,
29
- DropdownMenuItem,
30
- DropdownMenuSeparator,
31
- DropdownMenuSub,
32
- DropdownMenuSubContent,
33
- DropdownMenuSubTrigger,
34
- DropdownMenuTrigger,
35
- } from '../ui/primitives/dropdown-menu'
36
- import { TooltipProvider } from '../ui/primitives/tooltip'
37
- import { DaylightMenu, EdgesToggleItem, QualityMenu } from './render-settings-menu'
38
- import { SKY_DOCK_HEIGHT, SkyControl } from './sky-control'
39
-
40
- const wallModeConfig = {
41
- up: {
42
- icon: '/icons/room.webp',
43
- },
44
- cutaway: {
45
- icon: '/icons/wallcut.webp',
46
- },
47
- down: {
48
- icon: '/icons/walllow.webp',
49
- },
50
- } as const
51
-
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
- }
61
-
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
- }
67
-
68
- // Keep the dropdown open when flipping an in-place toggle row.
69
- const keepOpen = (event: Event, fn: () => void) => {
70
- event.preventDefault()
71
- fn()
72
- }
73
-
74
- // Scans + guides folded into one control. A baked GLB carries none of its own,
75
- // but the GLB viewer re-adds them from scene data when the privacy flags allow,
76
- // so the toggle shows for whichever exist.
77
- function VisibilityMenu({
78
- canShowScans,
79
- canShowGuides,
80
- }: {
81
- canShowScans: boolean
82
- canShowGuides: boolean
83
- }) {
84
- const i18n = usePascalI18n()
85
- const showScans = useViewer((s) => s.showScans)
86
- const showGuides = useViewer((s) => s.showGuides)
87
- return (
88
- <DropdownMenu>
89
- <DropdownMenuTrigger asChild>
90
- <ActionButton
91
- className="hover:bg-white/5 hover:text-foreground"
92
- label={i18n._({ id: 'viewer.visibility.title', message: 'Visibility' })}
93
- size="icon"
94
- tooltipClassName="dark"
95
- tooltipSide="top"
96
- variant="ghost"
97
- >
98
- <Eye className="h-5 w-5" />
99
- </ActionButton>
100
- </DropdownMenuTrigger>
101
- <DropdownMenuContent align="center" className="dark min-w-44" side="top">
102
- {canShowScans && (
103
- <DropdownMenuItem
104
- onSelect={(e) => keepOpen(e, () => useViewer.getState().setShowScans(!showScans))}
105
- >
106
- <img alt="" className="h-4 w-4 object-contain" src="/icons/mesh.webp" />
107
- <span>{i18n._({ id: 'viewer.visibility.scans', message: 'Scans' })}</span>
108
- {showScans ? (
109
- <Eye className="ml-auto h-4 w-4 text-foreground" />
110
- ) : (
111
- <EyeOff className="ml-auto h-4 w-4 text-muted-foreground" />
112
- )}
113
- </DropdownMenuItem>
114
- )}
115
- {canShowGuides && (
116
- <DropdownMenuItem
117
- onSelect={(e) => keepOpen(e, () => useViewer.getState().setShowGuides(!showGuides))}
118
- >
119
- <img alt="" className="h-4 w-4 object-contain" src="/icons/floorplan.webp" />
120
- <span>{i18n._({ id: 'viewer.visibility.guides', message: 'Guides' })}</span>
121
- {showGuides ? (
122
- <Eye className="ml-auto h-4 w-4 text-foreground" />
123
- ) : (
124
- <EyeOff className="ml-auto h-4 w-4 text-muted-foreground" />
125
- )}
126
- </DropdownMenuItem>
127
- )}
128
- </DropdownMenuContent>
129
- </DropdownMenu>
130
- )
131
- }
132
-
133
- // One "Display" button gathering shadows, camera projection, colors, render
134
- // mode, scene theme and edges.
135
- function DisplayMenu() {
136
- const i18n = usePascalI18n()
137
- const cameraMode = useViewer((s) => s.cameraMode)
138
- const textures = useViewer((s) => s.textures)
139
- const shadows = useViewer((s) => s.shadows)
140
- const sceneTheme = useViewer((s) => s.sceneTheme)
141
- const activeTheme = getSceneTheme(sceneTheme)
142
- return (
143
- <DropdownMenu>
144
- <DropdownMenuTrigger asChild>
145
- <ActionButton
146
- className="hover:bg-white/5 hover:text-foreground"
147
- label={i18n._({ id: 'viewer.display.title', message: 'Display settings' })}
148
- size="icon"
149
- tooltipClassName="dark"
150
- tooltipSide="top"
151
- variant="ghost"
152
- >
153
- <SlidersHorizontal className="h-5 w-5" />
154
- </ActionButton>
155
- </DropdownMenuTrigger>
156
- <DropdownMenuContent align="center" className="dark min-w-56" side="top">
157
- <DropdownMenuItem
158
- onSelect={(e) => keepOpen(e, () => useViewer.getState().setShadows(!shadows))}
159
- >
160
- <Contrast className="h-4 w-4" />
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>
167
- </DropdownMenuItem>
168
- <DropdownMenuItem
169
- onSelect={(e) =>
170
- keepOpen(e, () =>
171
- useViewer
172
- .getState()
173
- .setCameraMode(cameraMode === 'perspective' ? 'orthographic' : 'perspective'),
174
- )
175
- }
176
- >
177
- <Camera className="h-4 w-4" />
178
- <span>{i18n._({ id: 'viewer.display.camera', message: 'Camera' })}</span>
179
- <span className="ml-auto text-muted-foreground text-xs">
180
- {cameraMode === 'perspective'
181
- ? i18n._({ id: 'viewer.display.perspective', message: 'Perspective' })
182
- : i18n._({ id: 'viewer.display.orthographic', message: 'Orthographic' })}
183
- </span>
184
- </DropdownMenuItem>
185
- <DropdownMenuItem
186
- onSelect={(e) => keepOpen(e, () => useViewer.getState().setTextures(!textures))}
187
- >
188
- {textures ? <Palette className="h-4 w-4" /> : <Square className="h-4 w-4" />}
189
- <span>{i18n._({ id: 'viewer.display.colors', message: 'Colors' })}</span>
190
- <span className="ml-auto text-muted-foreground text-xs">
191
- {textures
192
- ? i18n._({ id: 'viewer.display.colored', message: 'Colored' })
193
- : i18n._({ id: 'viewer.display.monochrome', message: 'Monochrome' })}
194
- </span>
195
- </DropdownMenuItem>
196
-
197
- <DropdownMenuSeparator />
198
-
199
- <QualityMenu subContentClassName="dark min-w-56" />
200
-
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
- )}
242
- </DropdownMenuContent>
243
- </DropdownMenu>
244
- )
245
- }
246
-
247
- export type ViewerControlsBarProps = {
248
- canShowScans?: boolean
249
- canShowGuides?: boolean
250
- /** A baked GLB is the active artifact: hide controls it can't honor (wall
251
- * modes aren't baked into the GLB). */
252
- glbActive?: boolean
253
- /** In GLB mode, whether scans/guides were re-added from scene data — so the
254
- * visibility control surfaces the matching toggle even though the artifact
255
- * itself carries none. */
256
- glbHasScans?: boolean
257
- glbHasGuides?: boolean
258
- walkthroughActive?: boolean
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
263
- className?: string
264
- }
265
-
266
- export const ViewerControlsBar = ({
267
- canShowScans = true,
268
- canShowGuides = true,
269
- glbActive = false,
270
- glbHasScans = false,
271
- glbHasGuides = false,
272
- walkthroughActive = false,
273
- onWalkthroughToggle,
274
- droneActive = false,
275
- onDroneToggle,
276
- className,
277
- }: ViewerControlsBarProps) => {
278
- const i18n = usePascalI18n()
279
- const levelMode = useViewer((s) => s.levelMode)
280
- const wallMode = useViewer((s) => s.wallMode)
281
- const textures = useViewer((s) => s.textures)
282
- const isMobile = useIsMobile()
283
- // Sessions may carry a stale mode outside the cycle (e.g. the retired
284
- // 'translucent'); render and cycle it as cutaway instead of crashing.
285
- const safeWallMode = (
286
- wallMode in wallModeConfig ? wallMode : 'cutaway'
287
- ) as keyof typeof wallModeConfig
288
-
289
- return (
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"
305
- >
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
- )}
316
-
317
- {/* Level mode */}
318
- <ActionButton
319
- className={
320
- levelMode === 'stacked'
321
- ? 'hover:bg-white/5 hover:text-amber-400'
322
- : 'bg-amber-500/20 text-amber-400'
323
- }
324
- label={i18n._({
325
- id: 'viewer.levels.label',
326
- message: 'Levels: {mode}',
327
- values: { mode: levelModeLabel(levelMode, i18n) },
328
- })}
329
- onClick={() => {
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')
334
- }}
335
- size="icon"
336
- tooltipClassName="dark"
337
- tooltipSide="top"
338
- variant="ghost"
339
- >
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
- )}
345
- </ActionButton>
346
-
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
- )}
379
-
380
- <div className="mx-1 h-5 w-px bg-border/40" />
381
-
382
- <DisplayMenu />
383
-
384
- <div className="mx-1 h-5 w-px bg-border/40" />
385
-
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>
410
-
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
- )}
437
-
438
- <div className="mx-1 h-5 w-px bg-border/40" />
439
-
440
- {/* Follow Pascal */}
441
- <FollowPascalToggle className="hidden sm:inline-flex" tooltipClassName="dark" />
442
-
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
- </>
495
- )
496
- }
@@ -1,42 +0,0 @@
1
- 'use client'
2
-
3
- import { usePascalI18n, useScene } from '@pascal-app/core'
4
- import { useViewer } from '@pascal-app/viewer'
5
- import { nodeDisplayLabel } from '../../lib/node-display-label'
6
- import type { DescriptorTranslator } from '../../lib/room-name-messages'
7
- import { type ViewerNodes, zoneLabel } from '../../lib/viewer-selection'
8
-
9
- /** The hovered node's name, for the bottom pill. A room reads as the room. */
10
- export function viewerHoverLabel(
11
- hoveredId: string | null,
12
- nodes: ViewerNodes,
13
- i18n: DescriptorTranslator,
14
- ): string | null {
15
- const node = hoveredId ? nodes[hoveredId] : undefined
16
- if (!node) return null
17
- return node.type === 'zone' ? zoneLabel(node, i18n) : nodeDisplayLabel(node)
18
- }
19
-
20
- /**
21
- * What a click would select, named in a pill above the controls bar. Every
22
- * viewer surface's hit source sets the hover (`applyViewerHover`); hidden in
23
- * the walkthrough and on devices without a hovering pointer. Names come from
24
- * the parametric scene store unless the surface passes its own graph.
25
- */
26
- export function ViewerHoverLabel({ nodes: nodesProp }: { nodes?: ViewerNodes }) {
27
- const i18n = usePascalI18n()
28
- const sceneNodes = useScene((s) => s.nodes)
29
- const nodes = nodesProp ?? sceneNodes
30
- const label = useViewer((s) =>
31
- s.walkthroughMode ? null : viewerHoverLabel(s.hoveredId, nodes, i18n),
32
- )
33
- if (!label) return null
34
- return (
35
- <div
36
- className="pointer-events-none absolute bottom-24 left-1/2 z-20 -translate-x-1/2 rounded-full border border-border/60 bg-background/90 px-3 py-1 text-foreground text-sm shadow-elevation-3 backdrop-blur [@media(hover:none)]:hidden"
37
- data-testid="viewer-hover-label"
38
- >
39
- {label}
40
- </div>
41
- )
42
- }