'use client' import { Icon as IconifyIcon } from '@iconify/react' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, useEditor, useSidebarStore, type ViewMode, } from '@pascal-app/editor' import { CLAY_PALETTE, type EdgeMode, getSceneTheme, SCENE_THEMES, useViewer, } from '@pascal-app/viewer' import { Box, Check, ChevronsLeft, ChevronsRight, Columns2, Contrast, Eye, EyeOff, Footprints, Grid2X2, Magnet, PenLine, SlidersHorizontal, Sparkles, SwatchBook, } from 'lucide-react' import Image from 'next/image' import { type ReactNode, useCallback } from 'react' import { flushSync } from 'react-dom' import { cn } from '@/lib/utils' import { Tooltip, TooltipContent, TooltipTrigger } from './toolbar-tooltip' const TOOLBAR_CONTAINER = 'inline-flex h-8 items-stretch overflow-hidden rounded-xl border border-border bg-background/90 shadow-2xl backdrop-blur-md' const TOOLBAR_BTN = 'flex w-8 items-center justify-center text-muted-foreground/80 transition-colors hover:bg-white/8 hover:text-foreground/90' function requestWalkthroughPointerLock() { const canvas = document.querySelector('[data-pascal-viewer-3d] canvas') if (!canvas) return if (!canvas.hasAttribute('tabindex')) { canvas.tabIndex = -1 } canvas.focus({ preventScroll: true }) if (document.pointerLockElement === canvas) return try { canvas.requestPointerLock?.() } catch { return } } function ToolbarTooltip({ children, label }: { children: ReactNode; label: string }) { return ( {children} {label} ) } const VIEW_MODES: { id: ViewMode; label: string; icon: React.ReactNode }[] = [ { id: '3d', label: '3D', icon: ( ), }, { id: '2d', label: '2D', icon: ( ), }, { id: 'split', label: 'Split', icon: , }, ] const levelModeOrder = ['stacked', 'exploded', 'solo'] as const const levelModeLabels: Record = { manual: 'Stack', stacked: 'Stack', exploded: 'Exploded', solo: 'Solo', } const wallModeOrder = ['cutaway', 'up', 'down', 'translucent'] as const const wallModeConfig: Record = { up: { icon: '/icons/room.webp', label: 'Full height' }, cutaway: { icon: '/icons/wallcut.webp', label: 'Cutaway' }, down: { icon: '/icons/walllow.webp', label: 'Low' }, translucent: { icon: '/icons/wall.webp', label: 'Translucent' }, } const SHADING_OPTIONS = [ { id: 'solid', name: 'Solid', detail: 'Flat and fast — no ambient occlusion', icon: Box }, { id: 'rendered', name: 'Rendered', detail: 'Full ambient occlusion', icon: Sparkles }, ] as const function ViewModeControl() { const viewMode = useEditor((state) => state.viewMode) const setViewMode = useEditor((state) => state.setViewMode) return (
{VIEW_MODES.map((mode) => { const isActive = viewMode === mode.id return ( ) })}
) } function CollapseSidebarButton() { const isCollapsed = useSidebarStore((state) => state.isCollapsed) const setIsCollapsed = useSidebarStore((state) => state.setIsCollapsed) const toggle = useCallback(() => { setIsCollapsed(!isCollapsed) }, [isCollapsed, setIsCollapsed]) return (
) } function LevelModeToggle() { const levelMode = useViewer((state) => state.levelMode) const setLevelMode = useViewer((state) => state.setLevelMode) const isDefault = levelMode === 'stacked' || levelMode === 'manual' const cycle = () => { if (levelMode === 'manual') { setLevelMode('stacked') return } const index = levelModeOrder.indexOf(levelMode as (typeof levelModeOrder)[number]) const next = levelModeOrder[(index + 1) % levelModeOrder.length] if (next) setLevelMode(next) } const label = `Levels: ${levelMode === 'manual' ? 'Manual' : (levelModeLabels[levelMode] ?? 'Stack')}` return ( ) } function WallModeToggle() { const wallMode = useViewer((state) => state.wallMode) const setWallMode = useViewer((state) => state.setWallMode) const config = wallModeConfig[wallMode] ?? wallModeConfig.cutaway! const cycle = () => { const index = wallModeOrder.indexOf(wallMode as (typeof wallModeOrder)[number]) const next = wallModeOrder[(index + 1) % wallModeOrder.length] if (next) setWallMode(next) } return ( ) } // One dropdown that gathers every "how the scene looks" control: grid, shadows, // camera projection, units, render mode, edges and scene theme. const EDGE_OPTIONS = [ { id: 'off', name: 'Off', detail: 'No edge lines' }, { id: 'soft', name: 'Soft', detail: 'Faint outline of major creases' }, { id: 'strong', name: 'Strong', detail: 'Crisp, opaque edge lines' }, ] as const satisfies readonly { id: EdgeMode; name: string; detail: string }[] const SUBMENU_CONTENT_CLASS = 'min-w-56 rounded-xl border-border/45 bg-popover/95 backdrop-blur-xl' function DisplayMenu() { const showGrid = useViewer((state) => state.showGrid) const setShowGrid = useViewer((state) => state.setShowGrid) const unit = useViewer((state) => state.unit) const setUnit = useViewer((state) => state.setUnit) const cameraMode = useViewer((state) => state.cameraMode) const setCameraMode = useViewer((state) => state.setCameraMode) const shading = useViewer((state) => state.shading) const setShading = useViewer((state) => state.setShading) const sceneTheme = useViewer((state) => state.sceneTheme) const setSceneTheme = useViewer((state) => state.setSceneTheme) const edges = useViewer((state) => state.edges) const setEdges = useViewer((state) => state.setEdges) const shadows = useViewer((state) => state.shadows) const setShadows = useViewer((state) => state.setShadows) const magneticSnap = useEditor((state) => state.magneticSnap) const setMagneticSnap = useEditor((state) => state.setMagneticSnap) const activeShading = SHADING_OPTIONS.find((option) => option.id === shading) ?? SHADING_OPTIONS[0] const activeEdges = EDGE_OPTIONS.find((option) => option.id === edges) ?? EDGE_OPTIONS[0] const activeTheme = getSceneTheme(sceneTheme) // Keep the menu open when flipping a toggle. const keepOpen = (event: Event, fn: () => void) => { event.preventDefault() fn() } return ( keepOpen(e, () => setShowGrid(!showGrid))}> Grid {showGrid ? ( ) : ( )} keepOpen(e, () => setMagneticSnap(!magneticSnap))}> Magnetic snap {magneticSnap ? 'On' : 'Off'} keepOpen(e, () => setShadows(!shadows))}> Shadows {shadows ? 'On' : 'Off'} keepOpen(e, () => setCameraMode(cameraMode === 'perspective' ? 'orthographic' : 'perspective'), ) } > Camera {cameraMode === 'perspective' ? 'Perspective' : 'Orthographic'} keepOpen(e, () => setUnit(unit === 'metric' ? 'imperial' : 'metric'))} > {unit === 'metric' ? 'm' : 'ft'} Units {unit === 'metric' ? 'Metric' : 'Imperial'} Render {activeShading.name} {SHADING_OPTIONS.map((option) => { const OptionIcon = option.icon return ( setShading(option.id)}>
{option.name} {option.detail}
{shading === option.id ? ( ) : null}
) })}
Edges {activeEdges.name} {EDGE_OPTIONS.map((option) => ( setEdges(option.id)}>
{option.name} {option.detail}
{edges === option.id ? : null}
))}
Theme {activeTheme.name} {SCENE_THEMES.map((theme) => { const swatches = (['wall', 'roof', 'floor', 'glazing'] as const).map( (role) => theme.clayTints?.[role] ?? CLAY_PALETTE[role], ) return ( setSceneTheme(theme.id)}> {swatches.map((color, index) => ( ))} {theme.name} {sceneTheme === theme.id ? ( ) : null} ) })}
) } function WalkthroughButton() { const isFirstPersonMode = useEditor((state) => state.isFirstPersonMode) const setFirstPersonMode = useEditor((state) => state.setFirstPersonMode) const handleClick = useCallback(() => { if (isFirstPersonMode) { setFirstPersonMode(false) return } flushSync(() => setFirstPersonMode(true)) requestWalkthroughPointerLock() }, [isFirstPersonMode, setFirstPersonMode]) return ( ) } function PreviewButton() { return ( ) } export function CommunityViewerToolbarLeft() { return ( <> ) } export function CommunityViewerToolbarRight() { return (
) }