FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
CameraManager.tsx
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
3
4import { useEffect, useRef } from 'react';
5import { useThree, useFrame } from '@react-three/fiber';
6import {
7 useScenarioStore,
8 calculateInterpolatedPosition,
9} from '@/stores/scenarioStore';
10import { type MapControls as MapControlsImpl } from 'three-stdlib';
11import * as THREE from 'three';
12
13interface CameraManagerProps {
14 controlsRef: React.RefObject<MapControlsImpl | null>;
15}
16
17export default function CameraManager({ controlsRef }: CameraManagerProps) {
18 const { camera } = useThree();
19
20 const platforms = useScenarioStore((state) => state.platforms);
21 const viewControlAction = useScenarioStore(
22 (state) => state.viewControlAction
23 );
24 const clearViewControlAction = useScenarioStore(
25 (state) => state.clearViewControlAction
26 );
27
28 const lastActionTimestamp = useRef(0);
29
30 useEffect(() => {
31 if (
32 !controlsRef.current ||
33 viewControlAction.timestamp === lastActionTimestamp.current
34 ) {
35 return;
36 }
37
38 const controls = controlsRef.current;
39 const { type, targetId } = viewControlAction;
40
41 if (type === 'frame') {
42 const box = new THREE.Box3();
43 let hasPoints = false;
44
45 platforms.forEach((platform) => {
46 (platform.pathPoints ?? []).forEach((point: THREE.Vector3) => {
47 box.expandByPoint(point);
48 hasPoints = true;
49 });
50 });
51
52 if (
53 hasPoints &&
54 !box.isEmpty() &&
55 camera instanceof THREE.PerspectiveCamera
56 ) {
57 const center = new THREE.Vector3();
58 box.getCenter(center);
59 const size = new THREE.Vector3();
60 box.getSize(size);
61
62 const maxDim = Math.max(size.x, size.y, size.z);
63 const fov = camera.fov * (Math.PI / 180);
64 let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
65 cameraZ *= 1.5;
66
67 const newPos = new THREE.Vector3(
68 center.x,
69 center.y + cameraZ / 2,
70 center.z + cameraZ
71 );
72
73 camera.position.copy(newPos);
74 controls.target.copy(center);
75 controls.update();
76 }
77
78 lastActionTimestamp.current = viewControlAction.timestamp;
79 clearViewControlAction();
80 } else if (type === 'focus' && targetId) {
81 const platform = platforms.find((p) => p.id === targetId);
82 if (platform) {
83 const currentTime = useScenarioStore.getState().currentTime;
84 const position = calculateInterpolatedPosition(
85 platform,
86 currentTime
87 );
88 controls.target.copy(position);
89 controls.update();
90 }
91 lastActionTimestamp.current = viewControlAction.timestamp;
92 clearViewControlAction();
93 }
94 }, [
95 viewControlAction,
96 controlsRef,
97 camera,
98 platforms,
99 clearViewControlAction,
100 ]);
101 useFrame(() => {
102 if (
103 !controlsRef.current ||
104 viewControlAction.type !== 'follow' ||
105 !viewControlAction.targetId
106 ) {
107 return;
108 }
109
110 const controls = controlsRef.current;
111 const platform = platforms.find(
112 (p) => p.id === viewControlAction.targetId
113 );
114
115 if (platform) {
116 const position = calculateInterpolatedPosition(
117 platform,
118 useScenarioStore.getState().currentTime
119 );
120 controls.target.copy(position);
121 controls.update();
122 }
123 });
124
125 return null;
126}