'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, LayoutGrid, Magnet, PenLine, SlidersHorizontal, Sparkles, SwatchBook, } from 'lucide-react' import Image from 'next/image' import Link from 'next/link' import { type ReactNode, useCallback } from 'react' import { flushSync } from 'react-dom' import { withPascalBasePath } from '@/lib/pascal-base-path' 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: '分屏', icon: , }, ] const levelModeOrder = ['stacked', 'exploded', 'solo'] as const const levelModeLabels: Record = { manual: '堆叠', stacked: '堆叠', exploded: '分层', solo: '单层', } const wallModeOrder = ['cutaway', 'up', 'down', 'translucent'] as const const wallModeConfig: Record = { up: { icon: withPascalBasePath('/icons/room.webp'), label: '全高' }, cutaway: { icon: withPascalBasePath('/icons/wallcut.webp'), label: '剖切' }, down: { icon: withPascalBasePath('/icons/walllow.webp'), label: '低墙' }, translucent: { icon: withPascalBasePath('/icons/wall.webp'), label: '半透明' }, } const SHADING_OPTIONS = [ { id: 'solid', name: '实体', detail: '快速平面显示,不含环境光遮蔽', icon: Box }, { id: 'rendered', name: '渲染', detail: '完整环境光遮蔽效果', 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 AllScenesButton() { 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 = `楼层:${levelMode === 'manual' ? '手动' : (levelModeLabels[levelMode] ?? '堆叠')}` 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: '关闭', detail: '不显示边线' }, { id: 'soft', name: '柔和', detail: '淡化显示主要折线' }, { id: 'strong', name: '清晰', detail: '显示清晰不透明边线' }, ] as const satisfies readonly { id: EdgeMode; name: string; detail: string }[] const SCENE_THEME_LABELS: Record = { studio: '工作室', paper: '纸张', sunset: '日落', overcast: '阴天', blueprint: '蓝图', mediterranean: '地中海', twilight: '黄昏', night: '夜景', verdant: '绿意', } function getSceneThemeLabel(id: string, fallback: string) { return SCENE_THEME_LABELS[id] ?? fallback } 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))}> 网格 {showGrid ? ( ) : ( )} keepOpen(e, () => setMagneticSnap(!magneticSnap))}> 磁吸捕捉 {magneticSnap ? '开' : '关'} keepOpen(e, () => setShadows(!shadows))}> 阴影 {shadows ? '开' : '关'} keepOpen(e, () => setCameraMode(cameraMode === 'perspective' ? 'orthographic' : 'perspective'), ) } > 相机 {cameraMode === 'perspective' ? '透视' : '正交'} keepOpen(e, () => setUnit(unit === 'metric' ? 'imperial' : 'metric'))} > {unit === 'metric' ? 'm' : 'ft'} 单位 {unit === 'metric' ? '公制' : '英制'} 渲染 {activeShading.name} {SHADING_OPTIONS.map((option) => { const OptionIcon = option.icon return ( setShading(option.id)}>
{option.name} {option.detail}
{shading === option.id ? ( ) : null}
) })}
边线 {activeEdges.name} {EDGE_OPTIONS.map((option) => ( setEdges(option.id)}>
{option.name} {option.detail}
{edges === option.id ? : null}
))}
主题 {getSceneThemeLabel(activeTheme.id, 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) => ( ))} {getSceneThemeLabel(theme.id, 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 (
) }