control bar
This commit is contained in:
@@ -178,29 +178,3 @@ const Grid = ({
|
|||||||
</mesh>
|
</mesh>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const LevelModeSwitcher = () => {
|
|
||||||
const setLevelMode = useViewer((state) => state.setLevelMode)
|
|
||||||
const levelMode = useViewer((state) => state.levelMode)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
|
||||||
<button
|
|
||||||
className={`px-4 py-2 rounded ${
|
|
||||||
levelMode === 'exploded' ? 'bg-blue-500 text-white' : 'bg-white'
|
|
||||||
}`}
|
|
||||||
onClick={() => setLevelMode('exploded')}
|
|
||||||
>
|
|
||||||
Exploded
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`px-4 py-2 rounded ${
|
|
||||||
levelMode === 'stacked' ? 'bg-blue-500 text-white' : 'bg-white'
|
|
||||||
}`}
|
|
||||||
onClick={() => setLevelMode('stacked')}
|
|
||||||
>
|
|
||||||
Stacked
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -418,8 +418,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
const newRotationY = (currentRotation[1] ?? 0) + rotationDelta
|
const newRotationY = (currentRotation[1] ?? 0) + rotationDelta
|
||||||
draft.rotation = [currentRotation[0], newRotationY, currentRotation[2]]
|
draft.rotation = [currentRotation[0], newRotationY, currentRotation[2]]
|
||||||
|
|
||||||
// Ref + cursor mesh only — no store update during drag
|
// Ref + cursor mesh + item mesh — no store update during drag
|
||||||
cursorRef.current.rotation.y = newRotationY
|
cursorRef.current.rotation.y = newRotationY
|
||||||
|
const mesh = sceneRegistry.nodes.get(draft.id)
|
||||||
|
if (mesh) mesh.rotation.y = newRotationY
|
||||||
revalidate()
|
revalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import useEditor from "@/store/use-editor";
|
|||||||
import { AnimatePresence, motion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
import { ItemCatalog } from "../item-catalog/item-catalog";
|
import { ItemCatalog } from "../item-catalog/item-catalog";
|
||||||
import { FurnishTools } from "./furnish-tools";
|
import { FurnishTools } from "./furnish-tools";
|
||||||
// import { ViewToggles } from "./view-toggles";
|
import { ViewToggles } from "./view-toggles";
|
||||||
|
|
||||||
export function ActionMenu({ className }: { className?: string }) {
|
export function ActionMenu({ className }: { className?: string }) {
|
||||||
const phase = useEditor((state) => state.phase);
|
const phase = useEditor((state) => state.phase);
|
||||||
@@ -138,8 +138,8 @@ export function ActionMenu({ className }: { className?: string }) {
|
|||||||
<PhaseSwitcher />
|
<PhaseSwitcher />
|
||||||
<div className="mx-1 h-5 w-px bg-zinc-700" />
|
<div className="mx-1 h-5 w-px bg-zinc-700" />
|
||||||
<ControlModes />
|
<ControlModes />
|
||||||
{/* <div className="mx-1 h-5 w-px bg-zinc-700" /> */}
|
<div className="mx-1 h-5 w-px bg-zinc-700" />
|
||||||
{/* <ViewToggles /> */}
|
<ViewToggles />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
|
import { Box, Camera, Diamond, Image, Layers, Layers2 } from 'lucide-react'
|
||||||
|
import { Button } from '@/components/ui/primitives/button'
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/primitives/tooltip'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const levelModeLabels: Record<'stacked' | 'exploded' | 'solo', string> = {
|
||||||
|
stacked: 'Stacked',
|
||||||
|
exploded: 'Exploded',
|
||||||
|
solo: 'Solo',
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelModeOrder: ('stacked' | 'exploded' | 'solo')[] = ['stacked', 'exploded', 'solo']
|
||||||
|
|
||||||
|
export function ViewToggles() {
|
||||||
|
const cameraMode = useViewer((state) => state.cameraMode)
|
||||||
|
const setCameraMode = useViewer((state) => state.setCameraMode)
|
||||||
|
const levelMode = useViewer((state) => state.levelMode)
|
||||||
|
const setLevelMode = useViewer((state) => state.setLevelMode)
|
||||||
|
const showScans = useViewer((state) => state.showScans)
|
||||||
|
const setShowScans = useViewer((state) => state.setShowScans)
|
||||||
|
const showGuides = useViewer((state) => state.showGuides)
|
||||||
|
const setShowGuides = useViewer((state) => state.setShowGuides)
|
||||||
|
|
||||||
|
const toggleCameraMode = () => {
|
||||||
|
setCameraMode(cameraMode === 'perspective' ? 'orthographic' : 'perspective')
|
||||||
|
}
|
||||||
|
|
||||||
|
const cycleLevelMode = () => {
|
||||||
|
if (levelMode === 'manual') {
|
||||||
|
setLevelMode('stacked')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const currentIndex = levelModeOrder.indexOf(levelMode as 'stacked' | 'exploded' | 'solo')
|
||||||
|
const nextIndex = (currentIndex + 1) % levelModeOrder.length
|
||||||
|
const nextMode = levelModeOrder[nextIndex]
|
||||||
|
if (nextMode) setLevelMode(nextMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{/* Camera Mode */}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
'h-8 w-8 text-zinc-400 transition-all',
|
||||||
|
cameraMode === 'orthographic'
|
||||||
|
? 'bg-violet-500/20 text-violet-400'
|
||||||
|
: 'hover:bg-zinc-800',
|
||||||
|
)}
|
||||||
|
onClick={toggleCameraMode}
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
<Camera className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Camera: {cameraMode === 'perspective' ? 'Perspective' : 'Orthographic'}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Level Mode */}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
'h-8 w-8 text-zinc-400 transition-all',
|
||||||
|
levelMode !== 'stacked'
|
||||||
|
? 'bg-amber-500/20 text-amber-400'
|
||||||
|
: 'hover:bg-zinc-800',
|
||||||
|
)}
|
||||||
|
onClick={cycleLevelMode}
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
{levelMode === 'solo' && <Diamond className="h-4 w-4" />}
|
||||||
|
{levelMode === 'exploded' && <Layers2 className="h-4 w-4" />}
|
||||||
|
{(levelMode === 'stacked' || levelMode === 'manual') && <Layers className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Levels: {levelMode === 'manual' ? 'Manual' : levelModeLabels[levelMode as keyof typeof levelModeLabels]}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Show Scans */}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
'h-8 w-8 text-zinc-400 transition-all',
|
||||||
|
showScans
|
||||||
|
? 'bg-cyan-500/20 text-cyan-400'
|
||||||
|
: 'hover:bg-zinc-800',
|
||||||
|
)}
|
||||||
|
onClick={() => setShowScans(!showScans)}
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
<Box className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Scans: {showScans ? 'Visible' : 'Hidden'}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Show Guides */}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className={cn(
|
||||||
|
'h-8 w-8 text-zinc-400 transition-all',
|
||||||
|
showGuides
|
||||||
|
? 'bg-purple-500/20 text-purple-400'
|
||||||
|
: 'hover:bg-zinc-800',
|
||||||
|
)}
|
||||||
|
onClick={() => setShowGuides(!showGuides)}
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
<Image className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Guides: {showGuides ? 'Visible' : 'Hidden'}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import * as THREE from 'three/webgpu'
|
|||||||
import { LevelSystem } from '../../systems/level/level-system'
|
import { LevelSystem } from '../../systems/level/level-system'
|
||||||
import { SceneRenderer } from '../renderers/scene-renderer'
|
import { SceneRenderer } from '../renderers/scene-renderer'
|
||||||
import PostProcessing from './post-processing'
|
import PostProcessing from './post-processing'
|
||||||
|
import { ViewerCamera } from './viewer-camera'
|
||||||
|
|
||||||
declare module '@react-three/fiber' {
|
declare module '@react-three/fiber' {
|
||||||
interface ThreeElements extends ThreeToJSXElements<typeof THREE> {}
|
interface ThreeElements extends ThreeToJSXElements<typeof THREE> {}
|
||||||
@@ -31,6 +32,7 @@ const Viewer: React.FC<ViewerProps> = ({ children }) => {
|
|||||||
camera={{ position: [50, 50, 50], fov: 50 }}
|
camera={{ position: [50, 50, 50], fov: 50 }}
|
||||||
>
|
>
|
||||||
<color attach="background" args={['#ececec']} />
|
<color attach="background" args={['#ececec']} />
|
||||||
|
<ViewerCamera />
|
||||||
|
|
||||||
<directionalLight position={[10, 10, 5]} intensity={0.5} castShadow />
|
<directionalLight position={[10, 10, 5]} intensity={0.5} castShadow />
|
||||||
<Environment preset="sunset" environmentIntensity={0.3} />
|
<Environment preset="sunset" environmentIntensity={0.3} />
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { OrthographicCamera, PerspectiveCamera } from "@react-three/drei"
|
||||||
|
import useViewer from "../../store/use-viewer"
|
||||||
|
|
||||||
|
export const ViewerCamera = () => {
|
||||||
|
const cameraMode = useViewer((state) => state.cameraMode)
|
||||||
|
return cameraMode === 'perspective' ? (
|
||||||
|
<PerspectiveCamera far={1000} fov={50} makeDefault near={0.1} position={[10, 10, 10]} />
|
||||||
|
) : (
|
||||||
|
<OrthographicCamera far={1000} makeDefault near={-1000} position={[10, 10, 10]} zoom={20} />
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -34,6 +34,12 @@ type ViewerState = {
|
|||||||
levelMode: "stacked" | "exploded" | "solo" | "manual";
|
levelMode: "stacked" | "exploded" | "solo" | "manual";
|
||||||
setLevelMode: (mode: "stacked" | "exploded" | "solo" | "manual") => void;
|
setLevelMode: (mode: "stacked" | "exploded" | "solo" | "manual") => void;
|
||||||
|
|
||||||
|
showScans: boolean;
|
||||||
|
setShowScans: (show: boolean) => void;
|
||||||
|
|
||||||
|
showGuides: boolean;
|
||||||
|
setShowGuides: (show: boolean) => void;
|
||||||
|
|
||||||
// Smart selection update
|
// Smart selection update
|
||||||
setSelection: (updates: Partial<SelectionPath>) => void;
|
setSelection: (updates: Partial<SelectionPath>) => void;
|
||||||
resetSelection: () => void;
|
resetSelection: () => void;
|
||||||
@@ -52,6 +58,12 @@ const useViewer = create<ViewerState>()((set, get) => ({
|
|||||||
levelMode: "stacked",
|
levelMode: "stacked",
|
||||||
setLevelMode: (mode) => set({ levelMode: mode }),
|
setLevelMode: (mode) => set({ levelMode: mode }),
|
||||||
|
|
||||||
|
showScans: true,
|
||||||
|
setShowScans: (show) => set({ showScans: show }),
|
||||||
|
|
||||||
|
showGuides: true,
|
||||||
|
setShowGuides: (show) => set({ showGuides: show }),
|
||||||
|
|
||||||
setSelection: (updates) =>
|
setSelection: (updates) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newSelection = { ...state.selection, ...updates };
|
const newSelection = { ...state.selection, ...updates };
|
||||||
|
|||||||
@@ -9,14 +9,17 @@ const EXPLODED_GAP = 5
|
|||||||
export const LevelSystem = () => {
|
export const LevelSystem = () => {
|
||||||
useFrame((_, delta) => {
|
useFrame((_, delta) => {
|
||||||
const levelMode = useViewer.getState().levelMode
|
const levelMode = useViewer.getState().levelMode
|
||||||
|
const selectedLevel = useViewer.getState().selection.levelId
|
||||||
sceneRegistry.byType.level.forEach((levelId) => {
|
sceneRegistry.byType.level.forEach((levelId) => {
|
||||||
const obj = sceneRegistry.nodes.get(levelId)
|
const obj = sceneRegistry.nodes.get(levelId)
|
||||||
if (obj) {
|
if (obj) {
|
||||||
const level = useScene.getState().nodes[levelId as LevelNode['id']]
|
const level = useScene.getState().nodes[levelId as LevelNode['id']]
|
||||||
const targetY =
|
const targetY =
|
||||||
((level as any).level || 0) *
|
((level as any).level || 0) *
|
||||||
(LEVEL_HEIGHT + (levelMode === 'stacked' ? 0 : EXPLODED_GAP))
|
(LEVEL_HEIGHT + (levelMode === 'exploded' ? EXPLODED_GAP: 0))
|
||||||
obj.position.y = lerp(obj.position.y, targetY, delta * 3)
|
obj.position.y = lerp(obj.position.y, targetY, delta * 3)
|
||||||
|
|
||||||
|
obj.visible = levelMode !== 'solo' || level?.id === selectedLevel || !selectedLevel
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user