feat: add camera presets (Overhead/Isometric/Room Focus)
- Create CameraPresets.tsx with preset views and UI selector - Replace free camera controls with preset-based system - Add smooth animated transitions between camera positions - Update Facility3DViewerPage with preset selector buttons - Remove unused WASD keyboard controls
This commit is contained in:
parent
d56b7f0b11
commit
dc0b357638
3 changed files with 211 additions and 87 deletions
154
frontend/src/components/facility3d/CameraPresets.tsx
Normal file
154
frontend/src/components/facility3d/CameraPresets.tsx
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { useThree } from '@react-three/fiber';
|
||||||
|
import { CameraControls } from '@react-three/drei';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
export type CameraPreset = 'OVERHEAD' | 'ISOMETRIC' | 'ROOM_FOCUS';
|
||||||
|
|
||||||
|
interface CameraPresetsProps {
|
||||||
|
preset: CameraPreset;
|
||||||
|
floorWidth: number;
|
||||||
|
floorHeight: number;
|
||||||
|
focusTarget?: { x: number; z: number } | null;
|
||||||
|
onReady?: (controls: CameraControls) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Camera preset configurations
|
||||||
|
const getPresetConfig = (
|
||||||
|
preset: CameraPreset,
|
||||||
|
floorWidth: number,
|
||||||
|
floorHeight: number,
|
||||||
|
focusTarget?: { x: number; z: number } | null
|
||||||
|
) => {
|
||||||
|
const centerX = 0;
|
||||||
|
const centerZ = 0;
|
||||||
|
const maxDim = Math.max(floorWidth, floorHeight);
|
||||||
|
|
||||||
|
switch (preset) {
|
||||||
|
case 'OVERHEAD':
|
||||||
|
// Top-down view - looking straight down
|
||||||
|
return {
|
||||||
|
position: [centerX, maxDim * 1.2, centerZ] as [number, number, number],
|
||||||
|
target: [centerX, 0, centerZ] as [number, number, number],
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'ISOMETRIC':
|
||||||
|
// 3/4 isometric view - classic strategy game angle
|
||||||
|
const isoDistance = maxDim * 0.8;
|
||||||
|
return {
|
||||||
|
position: [centerX + isoDistance, isoDistance * 0.7, centerZ + isoDistance] as [number, number, number],
|
||||||
|
target: [centerX, 0, centerZ] as [number, number, number],
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'ROOM_FOCUS':
|
||||||
|
// Focus on a specific room/area
|
||||||
|
const fx = focusTarget?.x ?? centerX;
|
||||||
|
const fz = focusTarget?.z ?? centerZ;
|
||||||
|
return {
|
||||||
|
position: [fx + 15, 12, fz + 15] as [number, number, number],
|
||||||
|
target: [fx, 0, fz] as [number, number, number],
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
position: [centerX + 40, 30, centerZ + 40] as [number, number, number],
|
||||||
|
target: [centerX, 0, centerZ] as [number, number, number],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CameraPresets({
|
||||||
|
preset,
|
||||||
|
floorWidth,
|
||||||
|
floorHeight,
|
||||||
|
focusTarget,
|
||||||
|
onReady,
|
||||||
|
}: CameraPresetsProps) {
|
||||||
|
const controlsRef = useRef<CameraControls>(null);
|
||||||
|
const lastPresetRef = useRef<string>('');
|
||||||
|
|
||||||
|
// Notify parent when controls are ready
|
||||||
|
useEffect(() => {
|
||||||
|
if (controlsRef.current && onReady) {
|
||||||
|
onReady(controlsRef.current);
|
||||||
|
}
|
||||||
|
}, [onReady]);
|
||||||
|
|
||||||
|
// Apply preset when it changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!controlsRef.current) return;
|
||||||
|
|
||||||
|
const presetKey = `${preset}-${focusTarget?.x ?? 0}-${focusTarget?.z ?? 0}`;
|
||||||
|
if (presetKey === lastPresetRef.current) return;
|
||||||
|
lastPresetRef.current = presetKey;
|
||||||
|
|
||||||
|
const config = getPresetConfig(preset, floorWidth, floorHeight, focusTarget);
|
||||||
|
|
||||||
|
// Smooth transition to new position
|
||||||
|
controlsRef.current.setLookAt(
|
||||||
|
config.position[0],
|
||||||
|
config.position[1],
|
||||||
|
config.position[2],
|
||||||
|
config.target[0],
|
||||||
|
config.target[1],
|
||||||
|
config.target[2],
|
||||||
|
true // Enable smooth transition
|
||||||
|
);
|
||||||
|
}, [preset, floorWidth, floorHeight, focusTarget]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CameraControls
|
||||||
|
ref={controlsRef}
|
||||||
|
// Restrict interactions - no free orbiting
|
||||||
|
enabled={true}
|
||||||
|
// Allow zoom but restrict rotation
|
||||||
|
minDistance={5}
|
||||||
|
maxDistance={Math.max(floorWidth, floorHeight) * 2}
|
||||||
|
// Limit polar angle (prevent going under the floor)
|
||||||
|
minPolarAngle={0}
|
||||||
|
maxPolarAngle={Math.PI / 2.2}
|
||||||
|
// Disable pan/truck for preset mode - camera is fixed to presets
|
||||||
|
dollySpeed={0.5}
|
||||||
|
truckSpeed={0}
|
||||||
|
// Smooth damping
|
||||||
|
smoothTime={0.3}
|
||||||
|
makeDefault
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI Component for preset selector buttons
|
||||||
|
interface CameraPresetSelectorProps {
|
||||||
|
current: CameraPreset;
|
||||||
|
onChange: (preset: CameraPreset) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CameraPresetSelector({ current, onChange }: CameraPresetSelectorProps) {
|
||||||
|
const presets: { id: CameraPreset; label: string; icon: string }[] = [
|
||||||
|
{ id: 'OVERHEAD', label: 'Overhead', icon: '⬇️' },
|
||||||
|
{ id: 'ISOMETRIC', label: 'Isometric', icon: '📐' },
|
||||||
|
{ id: 'ROOM_FOCUS', label: 'Room', icon: '🔍' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-1 bg-slate-800/80 rounded-lg p-1">
|
||||||
|
{presets.map(({ id, label, icon }) => (
|
||||||
|
<button
|
||||||
|
key={id}
|
||||||
|
onClick={() => onChange(id)}
|
||||||
|
className={`
|
||||||
|
px-3 py-1.5 rounded text-xs font-medium transition-all
|
||||||
|
${current === id
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'text-slate-300 hover:bg-slate-700'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
title={label}
|
||||||
|
>
|
||||||
|
<span className="mr-1">{icon}</span>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useFrame } from '@react-three/fiber';
|
import { Environment, ContactShadows } from '@react-three/drei';
|
||||||
import { CameraControls, Environment, ContactShadows } from '@react-three/drei';
|
import { CameraControls } from '@react-three/drei';
|
||||||
import type { Floor3DData, Room3D } from '../../lib/layoutApi';
|
import type { Floor3DData, Room3D } from '../../lib/layoutApi';
|
||||||
import { RoomObject } from './RoomObject';
|
import { RoomObject } from './RoomObject';
|
||||||
import { Beacon } from './Beacon';
|
import { Beacon } from './Beacon';
|
||||||
import { PlantPosition, VisMode } from './types';
|
import { PlantPosition, VisMode } from './types';
|
||||||
|
import { CameraPresets, CameraPreset } from './CameraPresets';
|
||||||
|
|
||||||
// Convert pixel coordinates to world units (same as RoomObject/SmartRack)
|
// Convert pixel coordinates to world units (same as RoomObject/SmartRack)
|
||||||
const SCALE = 0.1;
|
const SCALE = 0.1;
|
||||||
|
|
@ -12,9 +13,10 @@ const SCALE = 0.1;
|
||||||
interface FacilitySceneProps {
|
interface FacilitySceneProps {
|
||||||
data: Floor3DData;
|
data: Floor3DData;
|
||||||
visMode: VisMode;
|
visMode: VisMode;
|
||||||
targetView: { x: number; y: number; z: number; zoom?: boolean } | null;
|
cameraPreset: CameraPreset;
|
||||||
|
focusTarget?: { x: number; z: number } | null;
|
||||||
onPlantClick: (plant: PlantPosition) => void;
|
onPlantClick: (plant: PlantPosition) => void;
|
||||||
onControlsReady: (controls: CameraControls) => void;
|
onControlsReady?: (controls: CameraControls) => void;
|
||||||
highlightedTags?: string[];
|
highlightedTags?: string[];
|
||||||
dimMode?: boolean;
|
dimMode?: boolean;
|
||||||
beaconPosition?: [number, number, number] | null;
|
beaconPosition?: [number, number, number] | null;
|
||||||
|
|
@ -23,69 +25,20 @@ interface FacilitySceneProps {
|
||||||
export function FacilityScene({
|
export function FacilityScene({
|
||||||
data,
|
data,
|
||||||
visMode,
|
visMode,
|
||||||
targetView,
|
cameraPreset,
|
||||||
|
focusTarget,
|
||||||
onPlantClick,
|
onPlantClick,
|
||||||
onControlsReady,
|
onControlsReady,
|
||||||
highlightedTags = [],
|
highlightedTags = [],
|
||||||
dimMode = false,
|
dimMode = false,
|
||||||
beaconPosition = null,
|
beaconPosition = null,
|
||||||
}: FacilitySceneProps) {
|
}: FacilitySceneProps) {
|
||||||
const controlsRef = useRef<CameraControls>(null);
|
// Scale floor dimensions for centering and camera
|
||||||
const [keysPressed, setKeysPressed] = useState<Record<string, boolean>>({});
|
|
||||||
|
|
||||||
// Scale floor dimensions for centering
|
|
||||||
const scaledFloor = {
|
const scaledFloor = {
|
||||||
width: data.floor.width * SCALE,
|
width: data.floor.width * SCALE,
|
||||||
height: data.floor.height * SCALE,
|
height: data.floor.height * SCALE,
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (controlsRef.current) onControlsReady(controlsRef.current);
|
|
||||||
}, [controlsRef.current, onControlsReady]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (targetView && controlsRef.current) {
|
|
||||||
const { x, z, zoom } = targetView;
|
|
||||||
const dist = zoom ? 12 : 35;
|
|
||||||
const height = zoom ? 10 : 30;
|
|
||||||
controlsRef.current.setLookAt(x + dist, height, z + dist, x, 0, z, true);
|
|
||||||
}
|
|
||||||
}, [targetView]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleDown = (e: KeyboardEvent) => {
|
|
||||||
// Don't capture keys when an input/textarea is focused
|
|
||||||
const activeEl = document.activeElement;
|
|
||||||
if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['KeyW', 'KeyA', 'KeyS', 'KeyD', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.code)) {
|
|
||||||
e.preventDefault();
|
|
||||||
setKeysPressed(k => ({ ...k, [e.code]: true }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleUp = (e: KeyboardEvent) => {
|
|
||||||
setKeysPressed(k => ({ ...k, [e.code]: false }));
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('keydown', handleDown);
|
|
||||||
window.addEventListener('keyup', handleUp);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', handleDown);
|
|
||||||
window.removeEventListener('keyup', handleUp);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useFrame((_, delta) => {
|
|
||||||
if (!controlsRef.current) return;
|
|
||||||
const speed = 18 * delta;
|
|
||||||
if (keysPressed['KeyW'] || keysPressed['ArrowUp']) controlsRef.current.forward(speed, true);
|
|
||||||
if (keysPressed['KeyS'] || keysPressed['ArrowDown']) controlsRef.current.forward(-speed, true);
|
|
||||||
if (keysPressed['KeyA'] || keysPressed['ArrowLeft']) controlsRef.current.truck(-speed, 0, true);
|
|
||||||
if (keysPressed['KeyD'] || keysPressed['ArrowRight']) controlsRef.current.truck(speed, 0, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Environment preset="city" background={false} />
|
<Environment preset="city" background={false} />
|
||||||
|
|
@ -123,13 +76,13 @@ export function FacilityScene({
|
||||||
color="#000"
|
color="#000"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CameraControls
|
{/* Preset-based camera system */}
|
||||||
ref={controlsRef}
|
<CameraPresets
|
||||||
minDistance={4}
|
preset={cameraPreset}
|
||||||
maxDistance={120}
|
floorWidth={scaledFloor.width}
|
||||||
dollySpeed={0.8}
|
floorHeight={scaledFloor.height}
|
||||||
truckSpeed={1.5}
|
focusTarget={focusTarget}
|
||||||
makeDefault
|
onReady={onControlsReady}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,13 @@ import { useEffect, useState, Suspense, Component, ReactNode, useCallback } from
|
||||||
import { Canvas } from '@react-three/fiber';
|
import { Canvas } from '@react-three/fiber';
|
||||||
import { Html, CameraControls } from '@react-three/drei';
|
import { Html, CameraControls } from '@react-three/drei';
|
||||||
import { layoutApi, Floor3DData, Room3D, Position3D } from '../lib/layoutApi';
|
import { layoutApi, Floor3DData, Room3D, Position3D } from '../lib/layoutApi';
|
||||||
import { Loader2, ArrowLeft, Maximize, MousePointer2, Layers, Thermometer, Droplets, Activity, Leaf, Eye, EyeOff, Clock } from 'lucide-react';
|
import { Loader2, ArrowLeft, Maximize, MousePointer2 } from 'lucide-react';
|
||||||
import { Link, useSearchParams } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
import { FacilityScene } from '../components/facility3d/FacilityScene';
|
import { FacilityScene } from '../components/facility3d/FacilityScene';
|
||||||
import { PlantSearch } from '../components/facility3d/PlantSearch';
|
import { PlantSearch } from '../components/facility3d/PlantSearch';
|
||||||
import { TimelineSlider } from '../components/facility3d/TimelineSlider';
|
import { TimelineSlider } from '../components/facility3d/TimelineSlider';
|
||||||
import { PlantPosition, VisMode, COLORS } from '../components/facility3d/types';
|
import { PlantPosition, VisMode, COLORS } from '../components/facility3d/types';
|
||||||
|
import { CameraPreset, CameraPresetSelector } from '../components/facility3d/CameraPresets';
|
||||||
|
|
||||||
// --- Error Boundary ---
|
// --- Error Boundary ---
|
||||||
interface ErrorBoundaryState { hasError: boolean; error: Error | null; }
|
interface ErrorBoundaryState { hasError: boolean; error: Error | null; }
|
||||||
|
|
@ -37,8 +38,6 @@ export default function Facility3DViewerPage() {
|
||||||
const [status, setStatus] = useState('Initializing...');
|
const [status, setStatus] = useState('Initializing...');
|
||||||
const [floorData, setFloorData] = useState<Floor3DData | null>(null);
|
const [floorData, setFloorData] = useState<Floor3DData | null>(null);
|
||||||
const [selectedPlant, setSelectedPlant] = useState<PlantPosition | null>(null);
|
const [selectedPlant, setSelectedPlant] = useState<PlantPosition | null>(null);
|
||||||
const [targetView, setTargetView] = useState<{ x: number; y: number; z: number; zoom?: boolean } | null>(null);
|
|
||||||
const [_controls, setControls] = useState<CameraControls | null>(null);
|
|
||||||
const [visMode, setVisMode] = useState<VisMode>('STANDARD');
|
const [visMode, setVisMode] = useState<VisMode>('STANDARD');
|
||||||
|
|
||||||
// Phase 2: Search state
|
// Phase 2: Search state
|
||||||
|
|
@ -55,6 +54,10 @@ export default function Facility3DViewerPage() {
|
||||||
const [allFloors, setAllFloors] = useState<{ id: string; name: string; buildingName: string }[]>([]);
|
const [allFloors, setAllFloors] = useState<{ id: string; name: string; buildingName: string }[]>([]);
|
||||||
const [selectedFloorId, setSelectedFloorId] = useState<string | null>(null);
|
const [selectedFloorId, setSelectedFloorId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Camera preset state
|
||||||
|
const [cameraPreset, setCameraPreset] = useState<CameraPreset>('ISOMETRIC');
|
||||||
|
const [focusTarget, setFocusTarget] = useState<{ x: number; z: number } | null>(null);
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const targetedPlantTag = searchParams.get('plant');
|
const targetedPlantTag = searchParams.get('plant');
|
||||||
|
|
||||||
|
|
@ -76,15 +79,16 @@ export default function Facility3DViewerPage() {
|
||||||
for (const section of room.sections) {
|
for (const section of room.sections) {
|
||||||
const match = section.positions.find((p: Position3D) => p.plant?.tagNumber === targetedPlantTag);
|
const match = section.positions.find((p: Position3D) => p.plant?.tagNumber === targetedPlantTag);
|
||||||
if (match) {
|
if (match) {
|
||||||
const spacing = 0.5;
|
const SCALE = 0.1;
|
||||||
const x = room.posX + section.posX + (match.column * spacing) - floorData.floor.width / 2;
|
const x = (room.posX + section.posX + (match.column * 0.5)) * SCALE - (floorData.floor.width * SCALE / 2);
|
||||||
const z = room.posY + section.posY + (match.row * spacing) - floorData.floor.height / 2;
|
const z = (room.posY + section.posY + (match.row * 0.5)) * SCALE - (floorData.floor.height * SCALE / 2);
|
||||||
const y = 0.4 + (match.tier * 0.6);
|
const y = 0.4 + (match.tier * 0.6);
|
||||||
|
|
||||||
const plant: PlantPosition = { ...match, x, y, z };
|
const plant: PlantPosition = { ...match, x, y, z };
|
||||||
setSelectedPlant(plant);
|
setSelectedPlant(plant);
|
||||||
setTargetView({ x, y: 0, z, zoom: true });
|
setFocusTarget({ x, z });
|
||||||
setBeaconPosition([x, 0, z]);
|
setCameraPreset('ROOM_FOCUS');
|
||||||
|
setBeaconPosition([x, y, z]);
|
||||||
setHighlightedTags([targetedPlantTag]);
|
setHighlightedTags([targetedPlantTag]);
|
||||||
setDimMode(true);
|
setDimMode(true);
|
||||||
return;
|
return;
|
||||||
|
|
@ -138,14 +142,12 @@ export default function Facility3DViewerPage() {
|
||||||
|
|
||||||
const focusRoom = (room: Room3D) => {
|
const focusRoom = (room: Room3D) => {
|
||||||
if (!floorData) return;
|
if (!floorData) return;
|
||||||
const offsetX = -floorData.floor.width / 2;
|
const SCALE = 0.1;
|
||||||
const offsetZ = -floorData.floor.height / 2;
|
// Calculate focus target in scaled world coordinates
|
||||||
setTargetView({
|
const x = (room.posX + room.width / 2) * SCALE - (floorData.floor.width * SCALE / 2);
|
||||||
x: room.posX + room.width / 2 + offsetX,
|
const z = (room.posY + room.height / 2) * SCALE - (floorData.floor.height * SCALE / 2);
|
||||||
y: 0,
|
setFocusTarget({ x, z });
|
||||||
z: room.posY + room.height / 2 + offsetZ,
|
setCameraPreset('ROOM_FOCUS');
|
||||||
zoom: true,
|
|
||||||
});
|
|
||||||
// Clear search state when navigating by room
|
// Clear search state when navigating by room
|
||||||
setBeaconPosition(null);
|
setBeaconPosition(null);
|
||||||
setHighlightedTags([]);
|
setHighlightedTags([]);
|
||||||
|
|
@ -153,7 +155,8 @@ export default function Facility3DViewerPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetView = () => {
|
const resetView = () => {
|
||||||
setTargetView({ x: 0, y: 0, z: 0, zoom: false });
|
setCameraPreset('ISOMETRIC');
|
||||||
|
setFocusTarget(null);
|
||||||
setBeaconPosition(null);
|
setBeaconPosition(null);
|
||||||
setHighlightedTags([]);
|
setHighlightedTags([]);
|
||||||
setDimMode(false);
|
setDimMode(false);
|
||||||
|
|
@ -164,12 +167,13 @@ export default function Facility3DViewerPage() {
|
||||||
const handleSearchSelect = useCallback((result: any) => {
|
const handleSearchSelect = useCallback((result: any) => {
|
||||||
if (!floorData) return;
|
if (!floorData) return;
|
||||||
|
|
||||||
const spacing = 0.5;
|
const SCALE = 0.1;
|
||||||
const x = result.position.roomX + result.position.sectionX + (result.position.column * spacing) - floorData.floor.width / 2;
|
const x = (result.position.roomX + result.position.sectionX + (result.position.column * 0.5)) * SCALE - (floorData.floor.width * SCALE / 2);
|
||||||
const z = result.position.roomY + result.position.sectionY + (result.position.row * spacing) - floorData.floor.height / 2;
|
const z = (result.position.roomY + result.position.sectionY + (result.position.row * 0.5)) * SCALE - (floorData.floor.height * SCALE / 2);
|
||||||
const y = 0.4 + (result.position.tier * 0.6);
|
const y = 0.4 + (result.position.tier * 0.6);
|
||||||
|
|
||||||
setTargetView({ x, y: 0, z, zoom: true });
|
setFocusTarget({ x, z });
|
||||||
|
setCameraPreset('ROOM_FOCUS');
|
||||||
setBeaconPosition([x, y, z]);
|
setBeaconPosition([x, y, z]);
|
||||||
setHighlightedTags([result.label]);
|
setHighlightedTags([result.label]);
|
||||||
setDimMode(true);
|
setDimMode(true);
|
||||||
|
|
@ -236,6 +240,19 @@ export default function Facility3DViewerPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Camera Preset Selector */}
|
||||||
|
<div className="pointer-events-auto">
|
||||||
|
<CameraPresetSelector
|
||||||
|
current={cameraPreset}
|
||||||
|
onChange={(preset) => {
|
||||||
|
setCameraPreset(preset);
|
||||||
|
if (preset !== 'ROOM_FOCUS') {
|
||||||
|
setFocusTarget(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Legend */}
|
{/* Legend */}
|
||||||
<div className="flex gap-3 text-xs bg-black/60 p-2 rounded-lg backdrop-blur border border-white/10 pointer-events-auto">
|
<div className="flex gap-3 text-xs bg-black/60 p-2 rounded-lg backdrop-blur border border-white/10 pointer-events-auto">
|
||||||
{visMode === 'STANDARD' && (
|
{visMode === 'STANDARD' && (
|
||||||
|
|
@ -409,9 +426,9 @@ export default function Facility3DViewerPage() {
|
||||||
<FacilityScene
|
<FacilityScene
|
||||||
data={floorData}
|
data={floorData}
|
||||||
visMode={visMode}
|
visMode={visMode}
|
||||||
targetView={targetView}
|
cameraPreset={cameraPreset}
|
||||||
|
focusTarget={focusTarget}
|
||||||
onPlantClick={setSelectedPlant}
|
onPlantClick={setSelectedPlant}
|
||||||
onControlsReady={setControls}
|
|
||||||
highlightedTags={highlightedTags}
|
highlightedTags={highlightedTags}
|
||||||
dimMode={dimMode}
|
dimMode={dimMode}
|
||||||
beaconPosition={beaconPosition}
|
beaconPosition={beaconPosition}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue