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) => {
47 box.expandByPoint(
48 new THREE.Vector3(point.x, point.y, point.z)
49 );
50 hasPoints = true;
51 });
52 });
53
54 if (
55 hasPoints &&
56 !box.isEmpty() &&
57 camera instanceof THREE.PerspectiveCamera
58 ) {
59 const center = new THREE.Vector3();
60 box.getCenter(center);
61 const size = new THREE.Vector3();
62 box.getSize(size);
63
64 const maxDim = Math.max(size.x, size.y, size.z);
65 const fov = camera.fov * (Math.PI / 180);
66 let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2));
67 cameraZ *= 1.5;
68
69 const newPos = new THREE.Vector3(
70 center.x,
71 center.y + cameraZ / 2,
72 center.z + cameraZ
73 );
74
75 camera.position.copy(newPos);
76 controls.target.copy(center);
77 controls.update();
78 }
79
80 lastActionTimestamp.current = viewControlAction.timestamp;
81 clearViewControlAction();
82 } else if (type === 'focus' && targetId) {
83 const platform = platforms.find((p) => p.id === targetId);
84 if (platform) {
85 const currentTime = useScenarioStore.getState().currentTime;
86 const position = calculateInterpolatedPosition(
87 platform,
88 currentTime
89 );
90 controls.target.copy(position);
91 controls.update();
92 }
93 lastActionTimestamp.current = viewControlAction.timestamp;
94 clearViewControlAction();
95 }
96 }, [
97 viewControlAction,
98 controlsRef,
99 camera,
100 platforms,
101 clearViewControlAction,
102 ]);
103 useFrame(() => {
104 if (
105 !controlsRef.current ||
106 viewControlAction.type !== 'follow' ||
107 !viewControlAction.targetId
108 ) {
109 return;
110 }
111
112 const controls = controlsRef.current;
113 const platform = platforms.find(
114 (p) => p.id === viewControlAction.targetId
115 );
116
117 if (platform) {
118 const position = calculateInterpolatedPosition(
119 platform,
120 useScenarioStore.getState().currentTime
121 );
122 controls.target.copy(position);
123 controls.update();
124 }
125 });
126
127 return null;
128}