@pascal-app/editor 0.9.2 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -7
- package/src/components/editor/bake-exporter.tsx +3 -1
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +307 -212
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +66 -2
- package/src/components/editor/first-person/build-collider-world.ts +20 -0
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +295 -132
- package/src/components/editor/floating-action-menu.tsx +59 -65
- package/src/components/editor/floorplan-background-selection.test.ts +16 -2
- package/src/components/editor/floorplan-background-selection.ts +4 -18
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +931 -861
- package/src/components/editor/group-actions.ts +155 -15
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-rotate-handle.tsx +1 -1
- package/src/components/editor/group-selection-box-3d.tsx +1 -1
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +17 -7
- package/src/components/editor/index.tsx +42 -9
- package/src/components/editor/measurement-pill.tsx +6 -1
- package/src/components/editor/node-arrow-handles.tsx +51 -15
- package/src/components/editor/quick-measurement-card.tsx +9 -3
- package/src/components/editor/quick-measurement-hud.tsx +7 -1
- package/src/components/editor/selection-manager.tsx +3 -1
- package/src/components/editor/site-edge-labels.tsx +24 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -1
- package/src/components/editor/thumbnail-generator.tsx +66 -294
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-mesh-settle-epoch.ts +1 -0
- package/src/components/editor/wall-measurement-label.tsx +8 -16
- package/src/components/editor/wall-move-side-handles.tsx +193 -18
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +6 -4
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +39 -8
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +47 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +21 -5
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +93 -59
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +27 -0
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +75 -1
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +92 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +93 -28
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +279 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +621 -281
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +12 -7
- package/src/components/tools/fence/fence-drafting.ts +27 -0
- package/src/components/tools/item/use-draft-node.ts +131 -72
- package/src/components/tools/item/use-placement-coordinator.tsx +52 -4
- package/src/components/tools/registry/move-registry-node-tool.tsx +75 -4
- package/src/components/tools/roof/roof-tool.tsx +16 -0
- package/src/components/tools/shared/floor-stack-preview.ts +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/site/site-boundary-editor.tsx +13 -6
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-tool.tsx +98 -47
- package/src/components/tools/tool-manager.tsx +67 -9
- package/src/components/tools/wall/wall-drafting.test.ts +118 -1
- package/src/components/tools/wall/wall-drafting.ts +128 -9
- package/src/components/tools/wall/wall-snap-geometry.test.ts +8 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +70 -3
- package/src/components/tools/zone/zone-tool.tsx +16 -0
- package/src/components/ui/action-menu/control-modes.tsx +4 -1
- package/src/components/ui/action-menu/measurement-control.tsx +107 -28
- package/src/components/ui/command-palette/editor-commands.tsx +14 -1
- package/src/components/ui/controls/material-paint-panel.tsx +7 -1
- package/src/components/ui/controls/material-picker.tsx +86 -7
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +73 -19
- package/src/components/ui/helpers/helper-manager.tsx +46 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/panel-manager.tsx +14 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -2
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +14 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +16 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -0
- package/src/components/viewer/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +20 -655
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +53 -30
- package/src/index.tsx +137 -4
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +8 -1
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/direct-manipulation.test.ts +83 -0
- package/src/lib/direct-manipulation.ts +9 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +236 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +745 -131
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +2 -3
- package/src/lib/fresh-planar-placement.test.ts +44 -1
- package/src/lib/fresh-planar-placement.ts +21 -4
- package/src/lib/glb-export.test.ts +116 -1
- package/src/lib/glb-export.ts +204 -7
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +22 -0
- package/src/lib/interaction/overlay-policy.test.ts +2 -1
- package/src/lib/interaction/scope.ts +40 -8
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/measurements.test.ts +5 -0
- package/src/lib/measurements.ts +12 -1
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/scene-clipboard.test.ts +130 -1
- package/src/lib/scene-clipboard.ts +237 -13
- package/src/lib/selection-routing.test.ts +16 -1
- package/src/lib/selection-routing.ts +5 -0
- package/src/lib/sfx-bus.ts +33 -2
- package/src/lib/sfx-player.test.ts +148 -6
- package/src/lib/sfx-player.ts +156 -8
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -0
- package/src/lib/snapping-mode.test.ts +29 -2
- package/src/lib/snapping-mode.ts +15 -3
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +57 -85
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +184 -15
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-fence-curve-draft.ts +38 -8
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +54 -4
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +23 -3
- package/src/store/use-interaction-scope.ts +8 -0
- package/src/store/use-measurement-draft.test.ts +29 -0
- package/src/store/use-measurement-draft.ts +33 -6
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-stair-build-preview.ts +12 -2
- package/src/lib/floorplan/selection-tool.ts +0 -271
|
@@ -1,51 +1,9 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { Icon } from '@iconify/react'
|
|
4
|
-
import {
|
|
5
|
-
type AnyNode,
|
|
6
|
-
type AnyNodeId,
|
|
7
|
-
type BuildingNode,
|
|
8
|
-
emitter,
|
|
9
|
-
getLevelDisplayName,
|
|
10
|
-
type LevelNode,
|
|
11
|
-
useScene,
|
|
12
|
-
type ZoneNode,
|
|
13
|
-
} from '@pascal-app/core'
|
|
14
|
-
import {
|
|
15
|
-
CLAY_PALETTE,
|
|
16
|
-
type EdgeMode,
|
|
17
|
-
getSceneTheme,
|
|
18
|
-
SCENE_THEMES,
|
|
19
|
-
useViewer,
|
|
20
|
-
} from '@pascal-app/viewer'
|
|
21
|
-
import {
|
|
22
|
-
ArrowLeft,
|
|
23
|
-
Box,
|
|
24
|
-
Camera,
|
|
25
|
-
Check,
|
|
26
|
-
ChevronRight,
|
|
27
|
-
Diamond,
|
|
28
|
-
Footprints,
|
|
29
|
-
Layers,
|
|
30
|
-
Palette,
|
|
31
|
-
PenLine,
|
|
32
|
-
Sparkles,
|
|
33
|
-
Square,
|
|
34
|
-
} from 'lucide-react'
|
|
35
|
-
import Link from 'next/link'
|
|
36
3
|
import { flushSync } from 'react-dom'
|
|
37
|
-
import { useShallow } from 'zustand/react/shallow'
|
|
38
|
-
import { cn } from '../lib/utils'
|
|
39
4
|
import useEditor from '../store/use-editor'
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
DropdownMenu,
|
|
43
|
-
DropdownMenuContent,
|
|
44
|
-
DropdownMenuItem,
|
|
45
|
-
DropdownMenuSeparator,
|
|
46
|
-
DropdownMenuTrigger,
|
|
47
|
-
} from './ui/primitives/dropdown-menu'
|
|
48
|
-
import { TooltipProvider } from './ui/primitives/tooltip'
|
|
5
|
+
import { ViewerControlsBar } from './viewer/viewer-controls-bar'
|
|
6
|
+
import { ViewerSceneHeader } from './viewer/viewer-scene-header'
|
|
49
7
|
|
|
50
8
|
type ProjectOwner = {
|
|
51
9
|
id: string
|
|
@@ -66,224 +24,16 @@ function requestWalkthroughPointerLock() {
|
|
|
66
24
|
if (document.pointerLockElement === canvas) return
|
|
67
25
|
|
|
68
26
|
try {
|
|
69
|
-
|
|
27
|
+
// The request can also reject ASYNC (browser cooldown after a recent
|
|
28
|
+
// unlock) — swallow it like the P-resume path; clicking the canvas
|
|
29
|
+
// re-requests once the cooldown passes.
|
|
30
|
+
const result = canvas.requestPointerLock?.() as Promise<void> | undefined
|
|
31
|
+
if (result && typeof result.catch === 'function') result.catch(() => {})
|
|
70
32
|
} catch {
|
|
71
33
|
return
|
|
72
34
|
}
|
|
73
35
|
}
|
|
74
36
|
|
|
75
|
-
const levelModeLabels: Record<'stacked' | 'exploded' | 'solo', string> = {
|
|
76
|
-
stacked: 'Stacked',
|
|
77
|
-
exploded: 'Exploded',
|
|
78
|
-
solo: 'Solo',
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const levelModeBadgeLabels: Record<'manual' | 'stacked' | 'exploded' | 'solo', string> = {
|
|
82
|
-
manual: 'Stack',
|
|
83
|
-
stacked: 'Stack',
|
|
84
|
-
exploded: 'Exploded',
|
|
85
|
-
solo: 'Solo',
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const wallModeConfig = {
|
|
89
|
-
up: {
|
|
90
|
-
icon: (props: any) => (
|
|
91
|
-
<img alt="Full Height" height={28} src="/icons/room.webp" width={28} {...props} />
|
|
92
|
-
),
|
|
93
|
-
label: 'Full Height',
|
|
94
|
-
},
|
|
95
|
-
cutaway: {
|
|
96
|
-
icon: (props: any) => (
|
|
97
|
-
<img alt="Cutaway" height={28} src="/icons/wallcut.webp" width={28} {...props} />
|
|
98
|
-
),
|
|
99
|
-
label: 'Cutaway',
|
|
100
|
-
},
|
|
101
|
-
down: {
|
|
102
|
-
icon: (props: any) => (
|
|
103
|
-
<img alt="Low" height={28} src="/icons/walllow.webp" width={28} {...props} />
|
|
104
|
-
),
|
|
105
|
-
label: 'Low',
|
|
106
|
-
},
|
|
107
|
-
translucent: {
|
|
108
|
-
icon: (props: any) => (
|
|
109
|
-
<img alt="Translucent" height={28} src="/icons/wall.png" width={28} {...props} />
|
|
110
|
-
),
|
|
111
|
-
label: 'Translucent',
|
|
112
|
-
},
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const SHADING_OPTIONS = [
|
|
116
|
-
{ id: 'solid', name: 'Solid', detail: 'Flat and fast — no ambient occlusion', icon: Box },
|
|
117
|
-
{ id: 'rendered', name: 'Rendered', detail: 'Full ambient occlusion', icon: Sparkles },
|
|
118
|
-
] as const
|
|
119
|
-
|
|
120
|
-
const TEXTURE_OPTIONS = [
|
|
121
|
-
{ id: true, name: 'Colored', detail: 'Show materials, textures & colors', icon: Palette },
|
|
122
|
-
{ id: false, name: 'Monochrome', detail: 'Flat clay surfaces by role', icon: Square },
|
|
123
|
-
] as const
|
|
124
|
-
|
|
125
|
-
function RenderModeMenu() {
|
|
126
|
-
const shading = useViewer((s) => s.shading)
|
|
127
|
-
const textures = useViewer((s) => s.textures)
|
|
128
|
-
const active = SHADING_OPTIONS.find((o) => o.id === shading) ?? SHADING_OPTIONS[0]
|
|
129
|
-
const ActiveIcon = active.icon
|
|
130
|
-
return (
|
|
131
|
-
<DropdownMenu>
|
|
132
|
-
<DropdownMenuTrigger asChild>
|
|
133
|
-
<ActionButton
|
|
134
|
-
className="text-muted-foreground/80 hover:bg-white/5 hover:text-foreground"
|
|
135
|
-
label={`Render: ${active.name}`}
|
|
136
|
-
size="icon"
|
|
137
|
-
tooltipSide="top"
|
|
138
|
-
variant="ghost"
|
|
139
|
-
>
|
|
140
|
-
<ActiveIcon className="h-6 w-6" />
|
|
141
|
-
</ActionButton>
|
|
142
|
-
</DropdownMenuTrigger>
|
|
143
|
-
<DropdownMenuContent align="center" className="min-w-56" side="top">
|
|
144
|
-
{SHADING_OPTIONS.map((option) => {
|
|
145
|
-
const OptionIcon = option.icon
|
|
146
|
-
return (
|
|
147
|
-
<DropdownMenuItem
|
|
148
|
-
key={option.id}
|
|
149
|
-
onSelect={() => useViewer.getState().setShading(option.id)}
|
|
150
|
-
>
|
|
151
|
-
<OptionIcon />
|
|
152
|
-
<div className="flex flex-col">
|
|
153
|
-
<span className="text-foreground">{option.name}</span>
|
|
154
|
-
<span className="text-muted-foreground text-xs">{option.detail}</span>
|
|
155
|
-
</div>
|
|
156
|
-
{shading === option.id ? <Check className="ml-auto text-foreground" /> : null}
|
|
157
|
-
</DropdownMenuItem>
|
|
158
|
-
)
|
|
159
|
-
})}
|
|
160
|
-
<DropdownMenuSeparator />
|
|
161
|
-
{TEXTURE_OPTIONS.map((option) => {
|
|
162
|
-
const OptionIcon = option.icon
|
|
163
|
-
return (
|
|
164
|
-
<DropdownMenuItem
|
|
165
|
-
key={option.name}
|
|
166
|
-
onSelect={() => useViewer.getState().setTextures(option.id)}
|
|
167
|
-
>
|
|
168
|
-
<OptionIcon />
|
|
169
|
-
<div className="flex flex-col">
|
|
170
|
-
<span className="text-foreground">{option.name}</span>
|
|
171
|
-
<span className="text-muted-foreground text-xs">{option.detail}</span>
|
|
172
|
-
</div>
|
|
173
|
-
{textures === option.id ? <Check className="ml-auto text-foreground" /> : null}
|
|
174
|
-
</DropdownMenuItem>
|
|
175
|
-
)
|
|
176
|
-
})}
|
|
177
|
-
</DropdownMenuContent>
|
|
178
|
-
</DropdownMenu>
|
|
179
|
-
)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function SceneThemeMenu() {
|
|
183
|
-
const sceneTheme = useViewer((s) => s.sceneTheme)
|
|
184
|
-
const active = getSceneTheme(sceneTheme)
|
|
185
|
-
return (
|
|
186
|
-
<DropdownMenu>
|
|
187
|
-
<DropdownMenuTrigger asChild>
|
|
188
|
-
<ActionButton
|
|
189
|
-
className="text-muted-foreground/80 hover:bg-white/5 hover:text-foreground"
|
|
190
|
-
label={`Theme: ${active.name}`}
|
|
191
|
-
size="icon"
|
|
192
|
-
tooltipSide="top"
|
|
193
|
-
variant="ghost"
|
|
194
|
-
>
|
|
195
|
-
<Icon color="currentColor" height={24} icon="lucide:swatch-book" width={24} />
|
|
196
|
-
</ActionButton>
|
|
197
|
-
</DropdownMenuTrigger>
|
|
198
|
-
<DropdownMenuContent align="center" className="min-w-48" side="top">
|
|
199
|
-
{SCENE_THEMES.map((sceneThemeOption) => {
|
|
200
|
-
const swatches = (['wall', 'roof', 'floor', 'glazing'] as const).map(
|
|
201
|
-
(role) => sceneThemeOption.clayTints?.[role] ?? CLAY_PALETTE[role],
|
|
202
|
-
)
|
|
203
|
-
return (
|
|
204
|
-
<DropdownMenuItem
|
|
205
|
-
key={sceneThemeOption.id}
|
|
206
|
-
onSelect={() => useViewer.getState().setSceneTheme(sceneThemeOption.id)}
|
|
207
|
-
>
|
|
208
|
-
<span
|
|
209
|
-
className="grid h-5 w-5 shrink-0 grid-cols-2 overflow-hidden rounded-sm border border-black/10"
|
|
210
|
-
style={{ backgroundColor: sceneThemeOption.background }}
|
|
211
|
-
>
|
|
212
|
-
{swatches.map((color, index) => (
|
|
213
|
-
<span
|
|
214
|
-
key={`${sceneThemeOption.id}-${index}`}
|
|
215
|
-
style={{ backgroundColor: color }}
|
|
216
|
-
/>
|
|
217
|
-
))}
|
|
218
|
-
</span>
|
|
219
|
-
<span className="text-foreground">{sceneThemeOption.name}</span>
|
|
220
|
-
{sceneTheme === sceneThemeOption.id ? (
|
|
221
|
-
<Check className="ml-auto text-foreground" />
|
|
222
|
-
) : null}
|
|
223
|
-
</DropdownMenuItem>
|
|
224
|
-
)
|
|
225
|
-
})}
|
|
226
|
-
</DropdownMenuContent>
|
|
227
|
-
</DropdownMenu>
|
|
228
|
-
)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const EDGE_OPTIONS = [
|
|
232
|
-
{ id: 'off', name: 'Off', detail: 'No edge lines' },
|
|
233
|
-
{ id: 'soft', name: 'Soft', detail: 'Faint outline of major creases' },
|
|
234
|
-
{ id: 'strong', name: 'Strong', detail: 'Crisp, opaque edge lines' },
|
|
235
|
-
] as const satisfies readonly { id: EdgeMode; name: string; detail: string }[]
|
|
236
|
-
|
|
237
|
-
function EdgesMenu() {
|
|
238
|
-
const edges = useViewer((s) => s.edges)
|
|
239
|
-
const active = EDGE_OPTIONS.find((o) => o.id === edges) ?? EDGE_OPTIONS[0]
|
|
240
|
-
return (
|
|
241
|
-
<DropdownMenu>
|
|
242
|
-
<DropdownMenuTrigger asChild>
|
|
243
|
-
<ActionButton
|
|
244
|
-
className={
|
|
245
|
-
edges === 'off'
|
|
246
|
-
? 'text-muted-foreground/80 hover:bg-white/5 hover:text-foreground'
|
|
247
|
-
: 'bg-white/10 text-foreground'
|
|
248
|
-
}
|
|
249
|
-
label={`Edges: ${active.name}`}
|
|
250
|
-
size="icon"
|
|
251
|
-
tooltipSide="top"
|
|
252
|
-
variant="ghost"
|
|
253
|
-
>
|
|
254
|
-
<PenLine className="h-6 w-6" />
|
|
255
|
-
</ActionButton>
|
|
256
|
-
</DropdownMenuTrigger>
|
|
257
|
-
<DropdownMenuContent align="center" className="min-w-56" side="top">
|
|
258
|
-
{EDGE_OPTIONS.map((option) => (
|
|
259
|
-
<DropdownMenuItem
|
|
260
|
-
key={option.id}
|
|
261
|
-
onSelect={() => useViewer.getState().setEdges(option.id)}
|
|
262
|
-
>
|
|
263
|
-
<div className="flex flex-col">
|
|
264
|
-
<span className="text-foreground">{option.name}</span>
|
|
265
|
-
<span className="text-muted-foreground text-xs">{option.detail}</span>
|
|
266
|
-
</div>
|
|
267
|
-
{edges === option.id ? <Check className="ml-auto text-foreground" /> : null}
|
|
268
|
-
</DropdownMenuItem>
|
|
269
|
-
))}
|
|
270
|
-
</DropdownMenuContent>
|
|
271
|
-
</DropdownMenu>
|
|
272
|
-
)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const getNodeName = (node: AnyNode): string => {
|
|
276
|
-
if ('name' in node && node.name) return node.name
|
|
277
|
-
if (node.type === 'wall') return 'Wall'
|
|
278
|
-
if (node.type === 'fence') return 'Fence'
|
|
279
|
-
if (node.type === 'item') return (node as { asset: { name: string } }).asset?.name || 'Item'
|
|
280
|
-
if (node.type === 'slab') return 'Slab'
|
|
281
|
-
if (node.type === 'ceiling') return 'Ceiling'
|
|
282
|
-
if (node.type === 'roof') return 'Roof'
|
|
283
|
-
if (node.type === 'roof-segment') return 'Roof Segment'
|
|
284
|
-
return node.type
|
|
285
|
-
}
|
|
286
|
-
|
|
287
37
|
interface ViewerOverlayProps {
|
|
288
38
|
projectName?: string | null
|
|
289
39
|
owner?: ProjectOwner | null
|
|
@@ -298,401 +48,16 @@ export const ViewerOverlay = ({
|
|
|
298
48
|
canShowScans = true,
|
|
299
49
|
canShowGuides = true,
|
|
300
50
|
onBack,
|
|
301
|
-
}: ViewerOverlayProps) =>
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
)
|
|
315
|
-
const level = useScene((s) =>
|
|
316
|
-
selection.levelId ? (s.nodes[selection.levelId] as LevelNode | undefined) : null,
|
|
317
|
-
)
|
|
318
|
-
const zone = useScene((s) =>
|
|
319
|
-
selection.zoneId ? (s.nodes[selection.zoneId] as ZoneNode | undefined) : null,
|
|
320
|
-
)
|
|
321
|
-
const selectedNode = useScene((s) =>
|
|
322
|
-
firstSelectedId ? (s.nodes[firstSelectedId as AnyNodeId] as AnyNode | undefined) : null,
|
|
323
|
-
)
|
|
324
|
-
const levels = useScene(
|
|
325
|
-
useShallow((s) => {
|
|
326
|
-
if (!building) return []
|
|
327
|
-
return building.children
|
|
328
|
-
.map((id) => s.nodes[id as AnyNodeId] as LevelNode | undefined)
|
|
329
|
-
.filter((n): n is LevelNode => n?.type === 'level')
|
|
330
|
-
.sort((a, b) => a.level - b.level)
|
|
331
|
-
}),
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
const handleLevelClick = (levelId: LevelNode['id']) => {
|
|
335
|
-
// When switching levels, deselect zone and items
|
|
336
|
-
useViewer.getState().setSelection({ levelId })
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const handleBreadcrumbClick = (depth: 'root' | 'building' | 'level' | 'zone') => {
|
|
340
|
-
switch (depth) {
|
|
341
|
-
case 'root':
|
|
342
|
-
useViewer.getState().resetSelection()
|
|
343
|
-
break
|
|
344
|
-
case 'building':
|
|
345
|
-
useViewer.getState().setSelection({ levelId: null })
|
|
346
|
-
break
|
|
347
|
-
case 'level':
|
|
348
|
-
useViewer.getState().setSelection({ zoneId: null })
|
|
349
|
-
break
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return (
|
|
354
|
-
<>
|
|
355
|
-
{/* Unified top-left card */}
|
|
356
|
-
<div className="dark absolute top-4 left-4 z-20 flex flex-col gap-3 text-foreground">
|
|
357
|
-
<div className="pointer-events-auto flex min-w-[200px] flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 shadow-lg backdrop-blur-xl transition-colors duration-200 ease-out">
|
|
358
|
-
{/* Project info + back */}
|
|
359
|
-
<div className="flex items-center gap-3 px-3 py-2.5">
|
|
360
|
-
{onBack ? (
|
|
361
|
-
<button
|
|
362
|
-
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-white/10"
|
|
363
|
-
onClick={onBack}
|
|
364
|
-
>
|
|
365
|
-
<ArrowLeft className="h-4 w-4 text-muted-foreground" />
|
|
366
|
-
</button>
|
|
367
|
-
) : (
|
|
368
|
-
<Link
|
|
369
|
-
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-white/10"
|
|
370
|
-
href="/"
|
|
371
|
-
>
|
|
372
|
-
<ArrowLeft className="h-4 w-4 text-muted-foreground" />
|
|
373
|
-
</Link>
|
|
374
|
-
)}
|
|
375
|
-
<div className="min-w-0">
|
|
376
|
-
<div className="truncate font-medium text-foreground text-sm">
|
|
377
|
-
{projectName || 'Untitled'}
|
|
378
|
-
</div>
|
|
379
|
-
{owner?.username && (
|
|
380
|
-
<Link
|
|
381
|
-
className="text-muted-foreground text-xs transition-colors hover:text-foreground"
|
|
382
|
-
href={`/u/${owner.username}`}
|
|
383
|
-
>
|
|
384
|
-
@{owner.username}
|
|
385
|
-
</Link>
|
|
386
|
-
)}
|
|
387
|
-
</div>
|
|
388
|
-
</div>
|
|
389
|
-
|
|
390
|
-
{/* Breadcrumb — only shown when navigated into a building */}
|
|
391
|
-
{building && (
|
|
392
|
-
<div className="border-border/40 border-t px-3 py-2">
|
|
393
|
-
<div className="flex items-center gap-1.5 text-xs">
|
|
394
|
-
<button
|
|
395
|
-
className="text-muted-foreground transition-colors hover:text-foreground"
|
|
396
|
-
onClick={() => handleBreadcrumbClick('root')}
|
|
397
|
-
>
|
|
398
|
-
Site
|
|
399
|
-
</button>
|
|
400
|
-
|
|
401
|
-
{building && (
|
|
402
|
-
<>
|
|
403
|
-
<ChevronRight className="h-3 w-3 text-muted-foreground/50" />
|
|
404
|
-
<button
|
|
405
|
-
className={`truncate transition-colors ${level ? 'text-muted-foreground hover:text-foreground' : 'font-medium text-foreground'}`}
|
|
406
|
-
onClick={() => handleBreadcrumbClick('building')}
|
|
407
|
-
>
|
|
408
|
-
{building.name || 'Building'}
|
|
409
|
-
</button>
|
|
410
|
-
</>
|
|
411
|
-
)}
|
|
412
|
-
|
|
413
|
-
{level && (
|
|
414
|
-
<>
|
|
415
|
-
<ChevronRight className="h-3 w-3 text-muted-foreground/50" />
|
|
416
|
-
<button
|
|
417
|
-
className={`truncate transition-colors ${zone ? 'text-muted-foreground hover:text-foreground' : 'font-medium text-foreground'}`}
|
|
418
|
-
onClick={() => handleBreadcrumbClick('level')}
|
|
419
|
-
>
|
|
420
|
-
{getLevelDisplayName(level)}
|
|
421
|
-
</button>
|
|
422
|
-
</>
|
|
423
|
-
)}
|
|
424
|
-
|
|
425
|
-
{zone && (
|
|
426
|
-
<>
|
|
427
|
-
<ChevronRight className="h-3 w-3 text-muted-foreground/50" />
|
|
428
|
-
<span
|
|
429
|
-
className={`truncate transition-colors ${selectedNode ? 'text-muted-foreground' : 'font-medium text-foreground'}`}
|
|
430
|
-
>
|
|
431
|
-
{zone.name}
|
|
432
|
-
</span>
|
|
433
|
-
</>
|
|
434
|
-
)}
|
|
435
|
-
|
|
436
|
-
{selectedNode && zone && (
|
|
437
|
-
<>
|
|
438
|
-
<ChevronRight className="h-3 w-3 text-muted-foreground/50" />
|
|
439
|
-
<span className="truncate font-medium text-foreground">
|
|
440
|
-
{getNodeName(selectedNode)}
|
|
441
|
-
</span>
|
|
442
|
-
</>
|
|
443
|
-
)}
|
|
444
|
-
</div>
|
|
445
|
-
</div>
|
|
446
|
-
)}
|
|
447
|
-
</div>
|
|
448
|
-
|
|
449
|
-
{/* Level List (only when building is selected) */}
|
|
450
|
-
{building && levels.length > 0 && (
|
|
451
|
-
<div className="pointer-events-auto flex w-48 flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 py-1 shadow-lg backdrop-blur-xl transition-colors duration-200 ease-out">
|
|
452
|
-
<span className="px-3 py-2 font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
|
|
453
|
-
Levels
|
|
454
|
-
</span>
|
|
455
|
-
<div className="flex flex-col">
|
|
456
|
-
{levels.map((lvl) => {
|
|
457
|
-
const isSelected = lvl.id === selection.levelId
|
|
458
|
-
return (
|
|
459
|
-
<button
|
|
460
|
-
className={cn(
|
|
461
|
-
'group/row relative flex h-8 w-full cursor-pointer select-none items-center border-border/50 border-r border-r-transparent border-b px-3 text-sm transition-all duration-200',
|
|
462
|
-
isSelected
|
|
463
|
-
? 'border-r-3 border-r-white bg-accent/50 text-foreground'
|
|
464
|
-
: 'text-muted-foreground hover:bg-accent/30 hover:text-foreground',
|
|
465
|
-
)}
|
|
466
|
-
key={lvl.id}
|
|
467
|
-
onClick={() => handleLevelClick(lvl.id)}
|
|
468
|
-
>
|
|
469
|
-
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
470
|
-
<span
|
|
471
|
-
className={cn(
|
|
472
|
-
'flex h-4 w-4 shrink-0 items-center justify-center transition-all duration-200',
|
|
473
|
-
!isSelected && 'opacity-60 grayscale',
|
|
474
|
-
)}
|
|
475
|
-
>
|
|
476
|
-
<Layers className="h-3.5 w-3.5" />
|
|
477
|
-
</span>
|
|
478
|
-
<div className="min-w-0 flex-1 truncate text-left">
|
|
479
|
-
{lvl.name || `Level ${lvl.level}`}
|
|
480
|
-
</div>
|
|
481
|
-
</div>
|
|
482
|
-
</button>
|
|
483
|
-
)
|
|
484
|
-
})}
|
|
485
|
-
</div>
|
|
486
|
-
</div>
|
|
487
|
-
)}
|
|
488
|
-
</div>
|
|
489
|
-
|
|
490
|
-
{/* Controls Panel - Bottom Center */}
|
|
491
|
-
<div className="dark absolute bottom-6 left-1/2 z-20 -translate-x-1/2 text-foreground">
|
|
492
|
-
<TooltipProvider delayDuration={0}>
|
|
493
|
-
<div className="pointer-events-auto flex h-14 flex-row items-center justify-center gap-1.5 rounded-2xl border border-border/40 bg-background/95 p-1.5 shadow-lg backdrop-blur-xl transition-colors duration-200 ease-out">
|
|
494
|
-
{/* Scans and Guides Visibility */}
|
|
495
|
-
{canShowScans && (
|
|
496
|
-
<ActionButton
|
|
497
|
-
className={
|
|
498
|
-
showScans
|
|
499
|
-
? 'bg-white/10'
|
|
500
|
-
: 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0'
|
|
501
|
-
}
|
|
502
|
-
label={`Scans: ${showScans ? 'Visible' : 'Hidden'}`}
|
|
503
|
-
onClick={() => useViewer.getState().setShowScans(!showScans)}
|
|
504
|
-
size="icon"
|
|
505
|
-
tooltipSide="top"
|
|
506
|
-
variant="ghost"
|
|
507
|
-
>
|
|
508
|
-
<img
|
|
509
|
-
alt="Scans"
|
|
510
|
-
className="h-[28px] w-[28px] object-contain"
|
|
511
|
-
src="/icons/mesh.webp"
|
|
512
|
-
/>
|
|
513
|
-
</ActionButton>
|
|
514
|
-
)}
|
|
515
|
-
|
|
516
|
-
{canShowGuides && (
|
|
517
|
-
<ActionButton
|
|
518
|
-
className={
|
|
519
|
-
showGuides
|
|
520
|
-
? 'bg-white/10'
|
|
521
|
-
: 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0'
|
|
522
|
-
}
|
|
523
|
-
label={`Guides: ${showGuides ? 'Visible' : 'Hidden'}`}
|
|
524
|
-
onClick={() => useViewer.getState().setShowGuides(!showGuides)}
|
|
525
|
-
size="icon"
|
|
526
|
-
tooltipSide="top"
|
|
527
|
-
variant="ghost"
|
|
528
|
-
>
|
|
529
|
-
<img
|
|
530
|
-
alt="Guides"
|
|
531
|
-
className="h-[28px] w-[28px] object-contain"
|
|
532
|
-
src="/icons/floorplan.webp"
|
|
533
|
-
/>
|
|
534
|
-
</ActionButton>
|
|
535
|
-
)}
|
|
536
|
-
|
|
537
|
-
{(canShowScans || canShowGuides) && <div className="mx-1 h-5 w-px bg-border/40" />}
|
|
538
|
-
|
|
539
|
-
{/* Camera Mode */}
|
|
540
|
-
<ActionButton
|
|
541
|
-
className={
|
|
542
|
-
cameraMode === 'orthographic'
|
|
543
|
-
? 'bg-violet-500/20 text-violet-400'
|
|
544
|
-
: 'hover:bg-white/5 hover:text-violet-400'
|
|
545
|
-
}
|
|
546
|
-
label={`Camera: ${cameraMode === 'perspective' ? 'Perspective' : 'Orthographic'}`}
|
|
547
|
-
onClick={() =>
|
|
548
|
-
useViewer
|
|
549
|
-
.getState()
|
|
550
|
-
.setCameraMode(cameraMode === 'perspective' ? 'orthographic' : 'perspective')
|
|
551
|
-
}
|
|
552
|
-
size="icon"
|
|
553
|
-
tooltipSide="top"
|
|
554
|
-
variant="ghost"
|
|
555
|
-
>
|
|
556
|
-
<Camera className="h-6 w-6" />
|
|
557
|
-
</ActionButton>
|
|
558
|
-
|
|
559
|
-
<RenderModeMenu />
|
|
560
|
-
|
|
561
|
-
<SceneThemeMenu />
|
|
562
|
-
|
|
563
|
-
<EdgesMenu />
|
|
564
|
-
|
|
565
|
-
{/* Level Mode */}
|
|
566
|
-
<ActionButton
|
|
567
|
-
className={cn(
|
|
568
|
-
'p-0',
|
|
569
|
-
levelMode === 'stacked' || levelMode === 'manual'
|
|
570
|
-
? 'text-muted-foreground/80 hover:bg-white/5 hover:text-foreground'
|
|
571
|
-
: 'bg-white/10 text-foreground',
|
|
572
|
-
)}
|
|
573
|
-
label={`Levels: ${levelMode === 'manual' ? 'Manual' : levelModeLabels[levelMode as keyof typeof levelModeLabels]}`}
|
|
574
|
-
onClick={() => {
|
|
575
|
-
if (levelMode === 'manual') return useViewer.getState().setLevelMode('stacked')
|
|
576
|
-
const modes: ('stacked' | 'exploded' | 'solo')[] = ['stacked', 'exploded', 'solo']
|
|
577
|
-
const nextIndex = (modes.indexOf(levelMode as any) + 1) % modes.length
|
|
578
|
-
useViewer.getState().setLevelMode(modes[nextIndex] ?? 'stacked')
|
|
579
|
-
}}
|
|
580
|
-
size="icon"
|
|
581
|
-
tooltipSide="top"
|
|
582
|
-
variant="ghost"
|
|
583
|
-
>
|
|
584
|
-
<span className="relative flex h-full w-full items-center justify-center pb-1">
|
|
585
|
-
{levelMode === 'solo' && <Diamond className="h-6 w-6" />}
|
|
586
|
-
{levelMode === 'exploded' && (
|
|
587
|
-
<Icon color="currentColor" height={24} icon="charm:stack-pop" width={24} />
|
|
588
|
-
)}
|
|
589
|
-
{(levelMode === 'stacked' || levelMode === 'manual') && (
|
|
590
|
-
<Icon color="currentColor" height={24} icon="charm:stack-push" width={24} />
|
|
591
|
-
)}
|
|
592
|
-
<span
|
|
593
|
-
aria-hidden="true"
|
|
594
|
-
className="pointer-events-none absolute right-1 bottom-1 left-1 rounded border border-border/50 bg-background/70 px-0.5 py-[2px] text-center font-medium font-pixel text-[8px] text-foreground/85 leading-none tracking-[-0.02em] backdrop-blur-sm"
|
|
595
|
-
>
|
|
596
|
-
{levelModeBadgeLabels[levelMode]}
|
|
597
|
-
</span>
|
|
598
|
-
</span>
|
|
599
|
-
</ActionButton>
|
|
600
|
-
|
|
601
|
-
{/* Wall Mode */}
|
|
602
|
-
<ActionButton
|
|
603
|
-
className={
|
|
604
|
-
wallMode !== 'cutaway'
|
|
605
|
-
? 'bg-white/10'
|
|
606
|
-
: 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0'
|
|
607
|
-
}
|
|
608
|
-
label={`Walls: ${wallModeConfig[wallMode as keyof typeof wallModeConfig].label}`}
|
|
609
|
-
onClick={() => {
|
|
610
|
-
const modes: ('cutaway' | 'up' | 'down' | 'translucent')[] = [
|
|
611
|
-
'cutaway',
|
|
612
|
-
'up',
|
|
613
|
-
'down',
|
|
614
|
-
'translucent',
|
|
615
|
-
]
|
|
616
|
-
const nextIndex = (modes.indexOf(wallMode as any) + 1) % modes.length
|
|
617
|
-
useViewer.getState().setWallMode(modes[nextIndex] ?? 'cutaway')
|
|
618
|
-
}}
|
|
619
|
-
size="icon"
|
|
620
|
-
tooltipSide="top"
|
|
621
|
-
variant="ghost"
|
|
622
|
-
>
|
|
623
|
-
{(() => {
|
|
624
|
-
const Icon = wallModeConfig[wallMode as keyof typeof wallModeConfig].icon
|
|
625
|
-
return <Icon className="h-[28px] w-[28px]" />
|
|
626
|
-
})()}
|
|
627
|
-
</ActionButton>
|
|
628
|
-
|
|
629
|
-
<div className="mx-1 h-5 w-px bg-border/40" />
|
|
630
|
-
|
|
631
|
-
{/* Camera Actions */}
|
|
632
|
-
<ActionButton
|
|
633
|
-
className="group hidden hover:bg-white/5 sm:inline-flex"
|
|
634
|
-
label="Orbit Left"
|
|
635
|
-
onClick={() => emitter.emit('camera-controls:orbit-ccw')}
|
|
636
|
-
size="icon"
|
|
637
|
-
tooltipSide="top"
|
|
638
|
-
variant="ghost"
|
|
639
|
-
>
|
|
640
|
-
<img
|
|
641
|
-
alt="Orbit Left"
|
|
642
|
-
className="h-[28px] w-[28px] -scale-x-100 object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
643
|
-
src="/icons/rotate.webp"
|
|
644
|
-
/>
|
|
645
|
-
</ActionButton>
|
|
646
|
-
|
|
647
|
-
<ActionButton
|
|
648
|
-
className="group hidden hover:bg-white/5 sm:inline-flex"
|
|
649
|
-
label="Orbit Right"
|
|
650
|
-
onClick={() => emitter.emit('camera-controls:orbit-cw')}
|
|
651
|
-
size="icon"
|
|
652
|
-
tooltipSide="top"
|
|
653
|
-
variant="ghost"
|
|
654
|
-
>
|
|
655
|
-
<img
|
|
656
|
-
alt="Orbit Right"
|
|
657
|
-
className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
658
|
-
src="/icons/rotate.webp"
|
|
659
|
-
/>
|
|
660
|
-
</ActionButton>
|
|
661
|
-
|
|
662
|
-
<ActionButton
|
|
663
|
-
className="group hover:bg-white/5"
|
|
664
|
-
label="Top View"
|
|
665
|
-
onClick={() => emitter.emit('camera-controls:top-view')}
|
|
666
|
-
size="icon"
|
|
667
|
-
tooltipSide="top"
|
|
668
|
-
variant="ghost"
|
|
669
|
-
>
|
|
670
|
-
<img
|
|
671
|
-
alt="Top View"
|
|
672
|
-
className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
|
|
673
|
-
src="/icons/topview.webp"
|
|
674
|
-
/>
|
|
675
|
-
</ActionButton>
|
|
676
|
-
|
|
677
|
-
<div className="mx-1 h-5 w-px bg-border/40" />
|
|
678
|
-
|
|
679
|
-
{/* First-person walkthrough */}
|
|
680
|
-
<ActionButton
|
|
681
|
-
className="hover:bg-white/5 hover:text-emerald-400"
|
|
682
|
-
label="Walkthrough"
|
|
683
|
-
onClick={() => {
|
|
684
|
-
flushSync(() => useEditor.getState().setFirstPersonMode(true))
|
|
685
|
-
requestWalkthroughPointerLock()
|
|
686
|
-
}}
|
|
687
|
-
size="icon"
|
|
688
|
-
tooltipSide="top"
|
|
689
|
-
variant="ghost"
|
|
690
|
-
>
|
|
691
|
-
<Footprints className="h-6 w-6" />
|
|
692
|
-
</ActionButton>
|
|
693
|
-
</div>
|
|
694
|
-
</TooltipProvider>
|
|
695
|
-
</div>
|
|
696
|
-
</>
|
|
697
|
-
)
|
|
698
|
-
}
|
|
51
|
+
}: ViewerOverlayProps) => (
|
|
52
|
+
<>
|
|
53
|
+
<ViewerSceneHeader onBack={onBack} owner={owner} projectName={projectName} />
|
|
54
|
+
<ViewerControlsBar
|
|
55
|
+
canShowGuides={canShowGuides}
|
|
56
|
+
canShowScans={canShowScans}
|
|
57
|
+
onWalkthroughToggle={() => {
|
|
58
|
+
flushSync(() => useEditor.getState().setFirstPersonMode(true))
|
|
59
|
+
requestWalkthroughPointerLock()
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
</>
|
|
63
|
+
)
|