fix z-index viewer

This commit is contained in:
wass08
2026-02-24 13:07:53 +09:00
parent 0dea6a2205
commit 0863f5e632
+265 -209
View File
@@ -1,9 +1,26 @@
'use client' 'use client'
import { type AnyNode, type AnyNodeId, type BuildingNode, type LevelNode, type ZoneNode, useScene } from '@pascal-app/core' import {
type AnyNode,
type AnyNodeId,
type BuildingNode,
type LevelNode,
useScene,
type ZoneNode,
} from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer' import { useViewer } from '@pascal-app/viewer'
import {
ArrowLeft,
Box,
ChevronRight,
Diamond,
Eye,
EyeOff,
Image,
Layers,
Layers2,
} from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
import { ArrowLeft, Box, ChevronRight, Diamond, Eye, EyeOff, Image, Layers, Layers2 } from 'lucide-react'
import type { ProjectOwner } from '@/features/community/lib/projects/types' import type { ProjectOwner } from '@/features/community/lib/projects/types'
const getNodeName = (node: AnyNode): string => { const getNodeName = (node: AnyNode): string => {
@@ -23,7 +40,12 @@ interface ViewerOverlayProps {
canShowGuides?: boolean canShowGuides?: boolean
} }
export const ViewerOverlay = ({ projectName, owner, canShowScans = true, canShowGuides = true }: ViewerOverlayProps) => { export const ViewerOverlay = ({
projectName,
owner,
canShowScans = true,
canShowGuides = true,
}: ViewerOverlayProps) => {
const selection = useViewer((s) => s.selection) const selection = useViewer((s) => s.selection)
const nodes = useScene((s) => s.nodes) const nodes = useScene((s) => s.nodes)
const showScans = useViewer((s) => s.showScans) const showScans = useViewer((s) => s.showScans)
@@ -32,20 +54,24 @@ export const ViewerOverlay = ({ projectName, owner, canShowScans = true, canShow
const levelMode = useViewer((s) => s.levelMode) const levelMode = useViewer((s) => s.levelMode)
const wallMode = useViewer((s) => s.wallMode) const wallMode = useViewer((s) => s.wallMode)
const building = selection.buildingId ? (nodes[selection.buildingId] as BuildingNode | undefined) : null const building = selection.buildingId
? (nodes[selection.buildingId] as BuildingNode | undefined)
: null
const level = selection.levelId ? (nodes[selection.levelId] as LevelNode | undefined) : null const level = selection.levelId ? (nodes[selection.levelId] as LevelNode | undefined) : null
const zone = selection.zoneId ? (nodes[selection.zoneId] as ZoneNode | undefined) : null const zone = selection.zoneId ? (nodes[selection.zoneId] as ZoneNode | undefined) : null
// Get the first selected item (if any) // Get the first selected item (if any)
const selectedNode = selection.selectedIds.length > 0 const selectedNode =
? (nodes[selection.selectedIds[0] as AnyNodeId] as AnyNode | undefined) selection.selectedIds.length > 0
: null ? (nodes[selection.selectedIds[0] as AnyNodeId] as AnyNode | undefined)
: null
// Get all levels for the selected building // Get all levels for the selected building
const levels = building?.children const levels =
.map((id) => nodes[id as AnyNodeId] as LevelNode | undefined) building?.children
.filter((n): n is LevelNode => n?.type === 'level') .map((id) => nodes[id as AnyNodeId] as LevelNode | undefined)
.sort((a, b) => a.level - b.level) ?? [] .filter((n): n is LevelNode => n?.type === 'level')
.sort((a, b) => a.level - b.level) ?? []
const handleLevelClick = (levelId: LevelNode['id']) => { const handleLevelClick = (levelId: LevelNode['id']) => {
// When switching levels, deselect zone and items // When switching levels, deselect zone and items
@@ -68,219 +94,249 @@ export const ViewerOverlay = ({ projectName, owner, canShowScans = true, canShow
return ( return (
<> <>
{/* Unified top-left card */} {/* Unified top-left card */}
<div className="absolute top-4 left-4 z-10 flex flex-col gap-3"> <div className="absolute top-4 left-4 z-20 flex flex-col gap-3">
<div className="bg-white/80 backdrop-blur-sm rounded-lg rounded-smooth shadow-[0_1px_4px_rgba(0,0,0,0.06),0_0_0_1px_rgba(0,0,0,0.03)] overflow-hidden"> <div className="bg-white/80 backdrop-blur-sm rounded-lg rounded-smooth shadow-[0_1px_4px_rgba(0,0,0,0.06),0_0_0_1px_rgba(0,0,0,0.03)] overflow-hidden">
{/* Project info + back */} {/* Project info + back */}
<div className="flex items-center gap-3 px-3 py-2"> <div className="flex items-center gap-3 px-3 py-2">
<Link <Link
href="/" href="/"
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md hover:bg-neutral-100 transition-colors" className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md hover:bg-neutral-100 transition-colors"
> >
<ArrowLeft className="h-4 w-4 text-neutral-500" /> <ArrowLeft className="h-4 w-4 text-neutral-500" />
</Link> </Link>
<div className="min-w-0"> <div className="min-w-0">
<div className="text-sm font-medium text-neutral-800 truncate"> <div className="text-sm font-medium text-neutral-800 truncate">
{projectName || 'Untitled'} {projectName || 'Untitled'}
</div>
{owner?.username && (
<Link
href={`/u/${owner.username}`}
className="text-xs text-neutral-500 hover:text-neutral-700 transition-colors"
>
@{owner.username}
</Link>
)}
</div> </div>
{owner?.username && (
<Link
href={`/u/${owner.username}`}
className="text-xs text-neutral-500 hover:text-neutral-700 transition-colors"
>
@{owner.username}
</Link>
)}
</div> </div>
</div>
{/* Breadcrumb — only shown when navigated into a building */} {/* Breadcrumb — only shown when navigated into a building */}
{building && ( {building && (
<div className="border-t border-neutral-100 px-3 py-1.5"> <div className="border-t border-neutral-100 px-3 py-1.5">
<div className="flex items-center gap-1 text-xs"> <div className="flex items-center gap-1 text-xs">
<button
onClick={() => handleBreadcrumbClick('root')}
className="text-neutral-500 hover:text-neutral-800 transition-colors"
>
Site
</button>
{building && (
<>
<ChevronRight className="w-3 h-3 text-neutral-400" />
<button <button
onClick={() => handleBreadcrumbClick('building')} onClick={() => handleBreadcrumbClick('root')}
className={`transition-colors truncate ${level ? 'text-neutral-500 hover:text-neutral-800' : 'text-neutral-800 font-medium'}`} className="text-neutral-500 hover:text-neutral-800 transition-colors"
> >
{building.name || 'Building'} Site
</button> </button>
</>
)}
{level && ( {building && (
<> <>
<ChevronRight className="w-3 h-3 text-neutral-400" /> <ChevronRight className="w-3 h-3 text-neutral-400" />
<button <button
onClick={() => handleBreadcrumbClick('level')} onClick={() => handleBreadcrumbClick('building')}
className={`transition-colors truncate ${zone ? 'text-neutral-500 hover:text-neutral-800' : 'text-neutral-800 font-medium'}`} className={`transition-colors truncate ${level ? 'text-neutral-500 hover:text-neutral-800' : 'text-neutral-800 font-medium'}`}
> >
{level.name || `Level ${level.level}`} {building.name || 'Building'}
</button> </button>
</> </>
)} )}
{zone && ( {level && (
<> <>
<ChevronRight className="w-3 h-3 text-neutral-400" /> <ChevronRight className="w-3 h-3 text-neutral-400" />
<span className={`transition-colors truncate ${selectedNode ? 'text-neutral-500' : 'text-neutral-800 font-medium'}`}> <button
{zone.name} onClick={() => handleBreadcrumbClick('level')}
</span> className={`transition-colors truncate ${zone ? 'text-neutral-500 hover:text-neutral-800' : 'text-neutral-800 font-medium'}`}
</> >
)} {level.name || `Level ${level.level}`}
</button>
</>
)}
{selectedNode && zone && ( {zone && (
<> <>
<ChevronRight className="w-3 h-3 text-neutral-400" /> <ChevronRight className="w-3 h-3 text-neutral-400" />
<span className="text-neutral-800 font-medium truncate">{getNodeName(selectedNode)}</span> <span
</> className={`transition-colors truncate ${selectedNode ? 'text-neutral-500' : 'text-neutral-800 font-medium'}`}
)} >
</div> {zone.name}
</div> </span>
)} </>
</div> )}
{/* Level List (only when building is selected) */} {selectedNode && zone && (
{building && levels.length > 0 && ( <>
<div className="flex flex-col gap-1 bg-white/80 backdrop-blur-sm rounded-lg p-2 shadow-[0_1px_4px_rgba(0,0,0,0.06),0_0_0_1px_rgba(0,0,0,0.03)] w-40"> <ChevronRight className="w-3 h-3 text-neutral-400" />
<span className="text-xs text-neutral-500 px-2 pb-1">Levels</span> <span className="text-neutral-800 font-medium truncate">
{levels.map((lvl) => ( {getNodeName(selectedNode)}
<button </span>
key={lvl.id} </>
onClick={() => handleLevelClick(lvl.id)} )}
className={`text-left px-2 py-1 rounded text-sm transition-colors ${ </div>
lvl.id === selection.levelId </div>
? 'bg-blue-500 text-white'
: 'text-neutral-700 hover:bg-neutral-100'
}`}
>
{lvl.name || `Level ${lvl.level}`}
</button>
))}
</div>
)}
</div>
{/* Controls Panel - Top Right */}
<div className="absolute top-4 right-4 z-10 flex flex-col gap-2">
{/* Visibility Controls */}
{(canShowScans || canShowGuides) && (
<div className="flex flex-col gap-1 bg-white/80 backdrop-blur-sm rounded-lg p-2 shadow-[0_1px_4px_rgba(0,0,0,0.06),0_0_0_1px_rgba(0,0,0,0.03)]">
<span className="text-xs text-neutral-500 px-2 pb-1">Visibility</span>
{canShowScans && (
<button
onClick={() => useViewer.getState().setShowScans(!showScans)}
className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${
showScans ? 'bg-blue-500 text-white' : 'text-neutral-700 hover:bg-neutral-100'
}`}
>
<Box className="w-4 h-4" />
3D Scans
</button>
)}
{canShowGuides && (
<button
onClick={() => useViewer.getState().setShowGuides(!showGuides)}
className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${
showGuides ? 'bg-blue-500 text-white' : 'text-neutral-700 hover:bg-neutral-100'
}`}
>
<Image className="w-4 h-4" />
Guides
</button>
)}
</div>
)}
{/* Camera Mode */}
<div className="flex flex-col gap-1 bg-white/80 backdrop-blur-sm rounded-lg p-2 shadow-[0_1px_4px_rgba(0,0,0,0.06),0_0_0_1px_rgba(0,0,0,0.03)]">
<span className="text-xs text-neutral-500 px-2 pb-1">Camera</span>
<button
onClick={() => useViewer.getState().setCameraMode(cameraMode === 'perspective' ? 'orthographic' : 'perspective')}
className="flex items-center gap-2 px-2 py-1 rounded text-sm text-neutral-700 hover:bg-neutral-100 transition-colors"
>
{cameraMode === 'perspective' ? (
<Eye className="w-4 h-4" />
) : (
<EyeOff className="w-4 h-4" />
)} )}
{cameraMode === 'perspective' ? 'Perspective' : 'Orthographic'} </div>
</button>
{/* Level List (only when building is selected) */}
{building && levels.length > 0 && (
<div className="flex flex-col gap-1 bg-white/80 backdrop-blur-sm rounded-lg p-2 shadow-[0_1px_4px_rgba(0,0,0,0.06),0_0_0_1px_rgba(0,0,0,0.03)] w-40">
<span className="text-xs text-neutral-500 px-2 pb-1">Levels</span>
{levels.map((lvl) => (
<button
key={lvl.id}
onClick={() => handleLevelClick(lvl.id)}
className={`text-left px-2 py-1 rounded text-sm transition-colors ${
lvl.id === selection.levelId
? 'bg-blue-500 text-white'
: 'text-neutral-700 hover:bg-neutral-100'
}`}
>
{lvl.name || `Level ${lvl.level}`}
</button>
))}
</div>
)}
</div> </div>
{/* Level Mode */} {/* Controls Panel - Top Right */}
<div className="flex flex-col gap-1 bg-white/80 backdrop-blur-sm rounded-lg p-2 shadow-[0_1px_4px_rgba(0,0,0,0.06),0_0_0_1px_rgba(0,0,0,0.03)]"> <div className="absolute top-4 right-4 z-20 flex flex-col gap-2">
<span className="text-xs text-neutral-500 px-2 pb-1">Level Mode</span> {/* Visibility Controls */}
<button {(canShowScans || canShowGuides) && (
onClick={() => useViewer.getState().setLevelMode('stacked')} <div className="flex flex-col gap-1 bg-white/80 backdrop-blur-sm rounded-lg p-2 shadow-[0_1px_4px_rgba(0,0,0,0.06),0_0_0_1px_rgba(0,0,0,0.03)]">
className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${ <span className="text-xs text-neutral-500 px-2 pb-1">Visibility</span>
levelMode === 'stacked' ? 'bg-blue-500 text-white' : 'text-neutral-700 hover:bg-neutral-100' {canShowScans && (
}`} <button
> onClick={() => useViewer.getState().setShowScans(!showScans)}
<Layers className="w-4 h-4" /> className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${
Stacked showScans ? 'bg-blue-500 text-white' : 'text-neutral-700 hover:bg-neutral-100'
</button> }`}
<button >
onClick={() => useViewer.getState().setLevelMode('exploded')} <Box className="w-4 h-4" />
className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${ 3D Scans
levelMode === 'exploded' ? 'bg-blue-500 text-white' : 'text-neutral-700 hover:bg-neutral-100' </button>
}`} )}
> {canShowGuides && (
<Layers2 className="w-4 h-4" /> <button
Exploded onClick={() => useViewer.getState().setShowGuides(!showGuides)}
</button> className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${
<button showGuides ? 'bg-blue-500 text-white' : 'text-neutral-700 hover:bg-neutral-100'
onClick={() => useViewer.getState().setLevelMode('solo')} }`}
className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${ >
levelMode === 'solo' ? 'bg-blue-500 text-white' : 'text-neutral-700 hover:bg-neutral-100' <Image className="w-4 h-4" />
}`} Guides
> </button>
<Diamond className="w-4 h-4" /> )}
Solo </div>
</button> )}
</div>
{/* Wall Mode */} {/* Camera Mode */}
<div className="flex flex-col gap-1 bg-white/80 backdrop-blur-sm rounded-lg p-2 shadow-[0_1px_4px_rgba(0,0,0,0.06),0_0_0_1px_rgba(0,0,0,0.03)]"> <div className="flex flex-col gap-1 bg-white/80 backdrop-blur-sm rounded-lg p-2 shadow-[0_1px_4px_rgba(0,0,0,0.06),0_0_0_1px_rgba(0,0,0,0.03)]">
<span className="text-xs text-neutral-500 px-2 pb-1">Wall Mode</span> <span className="text-xs text-neutral-500 px-2 pb-1">Camera</span>
<button <button
onClick={() => useViewer.getState().setWallMode('cutaway')} onClick={() =>
className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${ useViewer
wallMode === 'cutaway' ? 'bg-blue-500 text-white' : 'text-neutral-700 hover:bg-neutral-100' .getState()
}`} .setCameraMode(cameraMode === 'perspective' ? 'orthographic' : 'perspective')
> }
<img alt="Cutaway" height={16} src="/icons/wallcut.png" width={16} className="w-4 h-4" /> className="flex items-center gap-2 px-2 py-1 rounded text-sm text-neutral-700 hover:bg-neutral-100 transition-colors"
Cutaway >
</button> {cameraMode === 'perspective' ? (
<button <Eye className="w-4 h-4" />
onClick={() => useViewer.getState().setWallMode('up')} ) : (
className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${ <EyeOff className="w-4 h-4" />
wallMode === 'up' ? 'bg-blue-500 text-white' : 'text-neutral-700 hover:bg-neutral-100' )}
}`} {cameraMode === 'perspective' ? 'Perspective' : 'Orthographic'}
> </button>
<img alt="Full Height" height={16} src="/icons/room.png" width={16} className="w-4 h-4" /> </div>
Full Height
</button> {/* Level Mode */}
<button <div className="flex flex-col gap-1 bg-white/80 backdrop-blur-sm rounded-lg p-2 shadow-[0_1px_4px_rgba(0,0,0,0.06),0_0_0_1px_rgba(0,0,0,0.03)]">
onClick={() => useViewer.getState().setWallMode('down')} <span className="text-xs text-neutral-500 px-2 pb-1">Level Mode</span>
className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${ <button
wallMode === 'down' ? 'bg-blue-500 text-white' : 'text-neutral-700 hover:bg-neutral-100' onClick={() => useViewer.getState().setLevelMode('stacked')}
}`} className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${
> levelMode === 'stacked'
<img alt="Low" height={16} src="/icons/walllow.png" width={16} className="w-4 h-4" /> ? 'bg-blue-500 text-white'
Low : 'text-neutral-700 hover:bg-neutral-100'
</button> }`}
>
<Layers className="w-4 h-4" />
Stacked
</button>
<button
onClick={() => useViewer.getState().setLevelMode('exploded')}
className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${
levelMode === 'exploded'
? 'bg-blue-500 text-white'
: 'text-neutral-700 hover:bg-neutral-100'
}`}
>
<Layers2 className="w-4 h-4" />
Exploded
</button>
<button
onClick={() => useViewer.getState().setLevelMode('solo')}
className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${
levelMode === 'solo'
? 'bg-blue-500 text-white'
: 'text-neutral-700 hover:bg-neutral-100'
}`}
>
<Diamond className="w-4 h-4" />
Solo
</button>
</div>
{/* Wall Mode */}
<div className="flex flex-col gap-1 bg-white/80 backdrop-blur-sm rounded-lg p-2 shadow-[0_1px_4px_rgba(0,0,0,0.06),0_0_0_1px_rgba(0,0,0,0.03)]">
<span className="text-xs text-neutral-500 px-2 pb-1">Wall Mode</span>
<button
onClick={() => useViewer.getState().setWallMode('cutaway')}
className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${
wallMode === 'cutaway'
? 'bg-blue-500 text-white'
: 'text-neutral-700 hover:bg-neutral-100'
}`}
>
<img
alt="Cutaway"
height={16}
src="/icons/wallcut.png"
width={16}
className="w-4 h-4"
/>
Cutaway
</button>
<button
onClick={() => useViewer.getState().setWallMode('up')}
className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${
wallMode === 'up' ? 'bg-blue-500 text-white' : 'text-neutral-700 hover:bg-neutral-100'
}`}
>
<img
alt="Full Height"
height={16}
src="/icons/room.png"
width={16}
className="w-4 h-4"
/>
Full Height
</button>
<button
onClick={() => useViewer.getState().setWallMode('down')}
className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${
wallMode === 'down'
? 'bg-blue-500 text-white'
: 'text-neutral-700 hover:bg-neutral-100'
}`}
>
<img alt="Low" height={16} src="/icons/walllow.png" width={16} className="w-4 h-4" />
Low
</button>
</div>
</div> </div>
</div>
</> </>
) )
} }