@pascal-app/editor 1.1.0 → 1.1.2

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 (262) 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/node-icon-image.test.tsx +46 -0
  84. package/src/components/ui/node-icon-image.tsx +33 -1
  85. package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
  86. package/src/components/ui/panels/floor-foundation-panel.tsx +2 -2
  87. package/src/components/ui/panels/floor-opening-panel.tsx +4 -4
  88. package/src/components/ui/panels/node-display.ts +1 -1
  89. package/src/components/ui/panels/panel-manager.tsx +1 -1
  90. package/src/components/ui/panels/panel-wrapper.tsx +1 -1
  91. package/src/components/ui/panels/parametric-inspector.tsx +1 -1
  92. package/src/components/ui/panels/room-construction-rows.tsx +6 -6
  93. package/src/components/ui/panels/room-name-options.ts +1 -5
  94. package/src/components/ui/panels/room-panel.tsx +6 -8
  95. package/src/components/ui/primitives/color-dot.tsx +2 -2
  96. package/src/components/ui/primitives/opacity-control.tsx +2 -2
  97. package/src/components/ui/primitives/sidebar.tsx +3 -3
  98. package/src/components/ui/right-stack.tsx +1 -1
  99. package/src/components/ui/sidebar/icon-rail.tsx +1 -1
  100. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  101. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  102. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
  103. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
  104. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
  105. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -1
  106. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  107. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
  108. package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -2
  109. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
  110. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  111. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
  112. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
  113. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
  114. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
  115. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
  116. package/src/components/ui/sidebar/tab-bar.tsx +1 -1
  117. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
  118. package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
  119. package/src/components/viewer-overlay.tsx +1 -2
  120. package/src/hooks/use-auto-frame.ts +1 -2
  121. package/src/hooks/use-keyboard.ts +1 -2
  122. package/src/hooks/use-selected-room.ts +5 -7
  123. package/src/i18n/floor-footprint-name.ts +1 -1
  124. package/src/i18n/locales/en/messages.po +180 -182
  125. package/src/i18n/locales/es/messages.po +180 -182
  126. package/src/i18n/locales/fr/messages.po +180 -182
  127. package/src/i18n/locales/ja/messages.po +180 -182
  128. package/src/i18n/locales/zh-CN/messages.po +180 -182
  129. package/src/i18n/locales.test.ts +1 -1
  130. package/src/index.tsx +109 -131
  131. package/src/lib/below-grade.test.tsx +4 -4
  132. package/src/lib/camera-facing.test.ts +1 -1
  133. package/src/lib/camera-facing.ts +2 -3
  134. package/src/lib/editor-level-display.ts +1 -1
  135. package/src/lib/floor-footprints.test.ts +2 -2
  136. package/src/lib/floor-opening-draft.ts +1 -1
  137. package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
  138. package/src/lib/floorplan/annotation-visibility.ts +1 -1
  139. package/src/lib/floorplan/floorplan-export.test.ts +1 -1
  140. package/src/lib/floorplan/floorplan-export.tsx +8 -11
  141. package/src/lib/floorplan/floorplan-extension.ts +4 -157
  142. package/src/lib/floorplan/floorplan-mode.ts +2 -1
  143. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
  144. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
  145. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
  146. package/src/lib/floorplan/geometry.ts +2 -112
  147. package/src/lib/floorplan/index.ts +0 -9
  148. package/src/lib/floorplan/items.ts +2 -1
  149. package/src/lib/floorplan/schedules.ts +6 -3
  150. package/src/lib/floorplan/sheet-export.ts +7 -3
  151. package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
  152. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
  153. package/src/lib/follow-pascal-presentation.ts +1 -1
  154. package/src/lib/glb-export.ts +1 -1
  155. package/src/lib/material-paint.ts +1 -1
  156. package/src/lib/measurements.test.ts +2 -1
  157. package/src/lib/mezzanine-selection.test.ts +1 -1
  158. package/src/lib/paint-region-mode.ts +1 -1
  159. package/src/lib/point-ask/session.ts +1 -2
  160. package/src/lib/roof-duplication.test.ts +100 -0
  161. package/src/lib/roof-duplication.ts +30 -5
  162. package/src/lib/room-handle-drag.ts +2 -12
  163. package/src/lib/room-name-catalog.test.ts +2 -1
  164. package/src/lib/room-name-messages.test.ts +2 -1
  165. package/src/lib/room-name-messages.ts +1 -118
  166. package/src/lib/room-name-storage.test.ts +2 -1
  167. package/src/lib/room-selection-commands.ts +2 -2
  168. package/src/lib/room-selection.test.ts +6 -6
  169. package/src/lib/room-transform-session.ts +1 -2
  170. package/src/lib/room-zone-routing.ts +2 -2
  171. package/src/lib/selection-routing.test.ts +0 -28
  172. package/src/lib/selection-routing.ts +1 -40
  173. package/src/lib/sfx-bus.ts +8 -32
  174. package/src/lib/sfx-player.test.ts +72 -1
  175. package/src/lib/sfx-player.ts +49 -337
  176. package/src/lib/viewer-selection.test.ts +38 -10
  177. package/src/lib/wall-pull.test.ts +1 -1
  178. package/src/lib/wall-pull.ts +1 -27
  179. package/src/store/use-editor.tsx +17 -26
  180. package/src/store/use-floorplan-annotation-visibility.ts +5 -5
  181. package/src/store/use-wall-region-handles-selection.ts +1 -2
  182. package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
  183. package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
  184. package/src/components/editor/first-person/build-collider-world.ts +0 -549
  185. package/src/components/editor/first-person/ground-holes.test.ts +0 -128
  186. package/src/components/editor/first-person/ground-holes.ts +0 -130
  187. package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
  188. package/src/components/editor/first-person/mechanism-targets.ts +0 -88
  189. package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
  190. package/src/components/editor/first-person/terrain-collider.ts +0 -212
  191. package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
  192. package/src/components/editor/floorplan-camera-sync.ts +0 -213
  193. package/src/components/editor/localized-html.tsx +0 -22
  194. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
  195. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
  196. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
  197. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
  198. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
  199. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
  200. package/src/components/editor-2d/svg-paths.ts +0 -124
  201. package/src/components/ui/action-menu/action-button.tsx +0 -89
  202. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
  203. package/src/components/ui/primitives/button.tsx +0 -69
  204. package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
  205. package/src/components/ui/primitives/popover.tsx +0 -42
  206. package/src/components/ui/primitives/switch.tsx +0 -29
  207. package/src/components/ui/primitives/tooltip.tsx +0 -57
  208. package/src/components/ui/scene-loader.tsx +0 -73
  209. package/src/components/viewer/floorplan-compass-button.tsx +0 -67
  210. package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
  211. package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
  212. package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
  213. package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
  214. package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
  215. package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
  216. package/src/components/viewer/floorplan-preview.tsx +0 -1065
  217. package/src/components/viewer/render-settings-menu.tsx +0 -294
  218. package/src/components/viewer/sky-control.tsx +0 -710
  219. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
  220. package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
  221. package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
  222. package/src/components/viewer/viewer-controls-bar.tsx +0 -496
  223. package/src/components/viewer/viewer-hover-label.tsx +0 -42
  224. package/src/components/viewer/viewer-rooms.tsx +0 -248
  225. package/src/components/viewer/viewer-scene-header.tsx +0 -286
  226. package/src/components/viewer/viewer-selection-manager.tsx +0 -124
  227. package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
  228. package/src/components/viewer/viewer-stage-modes.ts +0 -49
  229. package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
  230. package/src/components/viewer/viewer-stage.test.tsx +0 -47
  231. package/src/components/viewer/viewer-stage.tsx +0 -221
  232. package/src/components/walkthrough-hud.tsx +0 -193
  233. package/src/hooks/use-mobile.ts +0 -19
  234. package/src/i18n/level-name.ts +0 -39
  235. package/src/i18n/scene-theme-name.ts +0 -16
  236. package/src/lib/below-grade.ts +0 -133
  237. package/src/lib/camera-pose.test.ts +0 -237
  238. package/src/lib/camera-pose.ts +0 -172
  239. package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
  240. package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
  241. package/src/lib/floorplan/floorplan-readonly.ts +0 -95
  242. package/src/lib/floorplan/marks.test.ts +0 -213
  243. package/src/lib/floorplan/marks.ts +0 -238
  244. package/src/lib/measurements.ts +0 -8
  245. package/src/lib/node-display-label.ts +0 -37
  246. package/src/lib/room-assembly-overlay.test.ts +0 -275
  247. package/src/lib/room-assembly-overlay.ts +0 -265
  248. package/src/lib/room-selection.ts +0 -417
  249. package/src/lib/scene-bounds.test.ts +0 -327
  250. package/src/lib/scene-bounds.ts +0 -233
  251. package/src/lib/typing-target.ts +0 -9
  252. package/src/lib/viewer-selection.ts +0 -595
  253. package/src/lib/zone-content.ts +0 -133
  254. package/src/store/camera-pose-store.test.ts +0 -29
  255. package/src/store/camera-pose-store.ts +0 -27
  256. package/src/store/navigation-sync-pose-store.test.ts +0 -34
  257. package/src/store/navigation-sync-pose-store.ts +0 -27
  258. package/src/store/use-audio.test.ts +0 -146
  259. package/src/store/use-audio.tsx +0 -169
  260. package/src/store/use-first-person-hud.ts +0 -40
  261. package/src/store/use-follow-pascal.test.ts +0 -62
  262. 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
- }