fix selection
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
|
||||
import { initSpatialGridSync, useScene } from '@pascal-app/core'
|
||||
import { Viewer } from '@pascal-app/viewer'
|
||||
import { OrbitControls } from '@react-three/drei'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ViewerCameraControls } from './viewer-camera-controls'
|
||||
import { ViewerOverlay } from './viewer-overlay'
|
||||
|
||||
export default function ViewerPage() {
|
||||
@@ -56,7 +56,7 @@ export default function ViewerPage() {
|
||||
<div className="relative h-screen w-full">
|
||||
<ViewerOverlay />
|
||||
<Viewer>
|
||||
<OrbitControls makeDefault />
|
||||
<ViewerCameraControls />
|
||||
</Viewer>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
'use client'
|
||||
|
||||
import { sceneRegistry, useScene } from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { CameraControls, CameraControlsImpl } from '@react-three/drei'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { Box3, Vector3 } from 'three'
|
||||
|
||||
const tempBox = new Box3()
|
||||
const tempCenter = new Vector3()
|
||||
const tempSize = new Vector3()
|
||||
|
||||
export const ViewerCameraControls = () => {
|
||||
const controls = useRef<CameraControlsImpl>(null!)
|
||||
const selection = useViewer((s) => s.selection)
|
||||
const nodes = useScene((s) => s.nodes)
|
||||
const cameraMode = useViewer((s) => s.cameraMode)
|
||||
const firstLoad = useRef(true)
|
||||
|
||||
// Get the deepest selected node ID (excluding selectedIds)
|
||||
const targetNodeId = selection.zoneId ?? selection.levelId ?? selection.buildingId
|
||||
|
||||
// Configure mouse buttons - same as editor
|
||||
const mouseButtons = useMemo(() => {
|
||||
const wheelAction =
|
||||
cameraMode === 'orthographic'
|
||||
? CameraControlsImpl.ACTION.ZOOM
|
||||
: CameraControlsImpl.ACTION.DOLLY
|
||||
|
||||
return {
|
||||
left: CameraControlsImpl.ACTION.NONE,
|
||||
middle: CameraControlsImpl.ACTION.SCREEN_PAN,
|
||||
right: CameraControlsImpl.ACTION.ROTATE,
|
||||
wheel: wheelAction,
|
||||
}
|
||||
}, [cameraMode])
|
||||
|
||||
useEffect(() => {
|
||||
if (!controls.current) return
|
||||
|
||||
// On first load, set a default camera position
|
||||
if (firstLoad.current) {
|
||||
firstLoad.current = false
|
||||
controls.current.setLookAt(30, 30, 30, 0, 0, 0, false)
|
||||
}
|
||||
|
||||
if (!targetNodeId) return
|
||||
|
||||
const node = nodes[targetNodeId]
|
||||
if (!node) return
|
||||
|
||||
// Check if node has a saved camera
|
||||
if (node.camera) {
|
||||
const { position, target } = node.camera
|
||||
controls.current.setLookAt(
|
||||
position[0],
|
||||
position[1],
|
||||
position[2],
|
||||
target[0],
|
||||
target[1],
|
||||
target[2],
|
||||
true
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate camera position based on the node's 3D object
|
||||
const object3D = sceneRegistry.nodes.get(targetNodeId)
|
||||
if (!object3D) return
|
||||
|
||||
// Compute bounding box
|
||||
tempBox.setFromObject(object3D)
|
||||
tempBox.getCenter(tempCenter)
|
||||
tempBox.getSize(tempSize)
|
||||
|
||||
// Calculate a good viewing distance based on the object size
|
||||
const maxDim = Math.max(tempSize.x, tempSize.y, tempSize.z)
|
||||
const distance = Math.max(maxDim * 2, 15)
|
||||
|
||||
// Position camera at an angle looking at the center
|
||||
const cameraPos = new Vector3(
|
||||
tempCenter.x + distance * 0.7,
|
||||
tempCenter.y + distance * 0.5,
|
||||
tempCenter.z + distance * 0.7
|
||||
)
|
||||
|
||||
controls.current.setLookAt(
|
||||
cameraPos.x,
|
||||
cameraPos.y,
|
||||
cameraPos.z,
|
||||
tempCenter.x,
|
||||
tempCenter.y,
|
||||
tempCenter.z,
|
||||
true
|
||||
)
|
||||
}, [targetNodeId, nodes])
|
||||
|
||||
return (
|
||||
<CameraControls
|
||||
ref={controls}
|
||||
maxDistance={100}
|
||||
minDistance={5}
|
||||
maxPolarAngle={Math.PI / 2 - 0.1}
|
||||
minPolarAngle={0}
|
||||
mouseButtons={mouseButtons}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { type AnyNode, type BuildingNode, type LevelNode, type ZoneNode, useScene } from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { Box, ChevronRight, Diamond, Eye, EyeOff, Image, Layers, Layers2 } from 'lucide-react'
|
||||
|
||||
const getNodeName = (node: AnyNode): string => {
|
||||
if ('name' in node && node.name) return node.name
|
||||
@@ -17,6 +17,10 @@ const getNodeName = (node: AnyNode): string => {
|
||||
export const ViewerOverlay = () => {
|
||||
const selection = useViewer((s) => s.selection)
|
||||
const nodes = useScene((s) => s.nodes)
|
||||
const showScans = useViewer((s) => s.showScans)
|
||||
const showGuides = useViewer((s) => s.showGuides)
|
||||
const cameraMode = useViewer((s) => s.cameraMode)
|
||||
const levelMode = useViewer((s) => s.levelMode)
|
||||
|
||||
const building = selection.buildingId ? (nodes[selection.buildingId] as BuildingNode | undefined) : null
|
||||
const level = selection.levelId ? (nodes[selection.levelId] as LevelNode | undefined) : null
|
||||
@@ -53,6 +57,7 @@ export const ViewerOverlay = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="absolute top-4 left-4 z-10 flex flex-col gap-3">
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
@@ -124,5 +129,80 @@ export const ViewerOverlay = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Controls Panel - Top Right */}
|
||||
<div className="absolute top-4 right-4 z-10 flex flex-col gap-2">
|
||||
{/* Visibility Controls */}
|
||||
<div className="flex flex-col gap-1 bg-white/80 backdrop-blur-sm rounded-lg p-2 shadow-sm border border-neutral-200">
|
||||
<span className="text-xs text-neutral-500 px-2 pb-1">Visibility</span>
|
||||
<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>
|
||||
<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-sm border border-neutral-200">
|
||||
<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'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Level Mode */}
|
||||
<div className="flex flex-col gap-1 bg-white/80 backdrop-blur-sm rounded-lg p-2 shadow-sm border border-neutral-200">
|
||||
<span className="text-xs text-neutral-500 px-2 pb-1">Level Mode</span>
|
||||
<button
|
||||
onClick={() => useViewer.getState().setLevelMode('stacked')}
|
||||
className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${
|
||||
levelMode === 'stacked' ? 'bg-blue-500 text-white' : 'text-neutral-700 hover:bg-neutral-100'
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+2213
-17565
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user