FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
utils.ts
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 { Euler, Vector3 } from 'three';
5import {
6 GlobalParameters,
7 Platform,
8 PlatformComponent,
9 ScenarioItem,
10 ScenarioState,
11} from './types';
12
13// Helper to set nested properties safely
14export const setPropertyByPath = (
15 obj: object,
16 path: string,
17 value: unknown
18): void => {
19 const keys = path.split('.');
20 const lastKey = keys.pop();
21 if (!lastKey) return;
22
23 let current: Record<string, unknown> = obj as Record<string, unknown>;
24
25 for (const key of keys) {
26 const next = current[key];
27 if (typeof next !== 'object' || next === null) {
28 // Path does not exist, so we cannot set the value.
29 return;
30 }
31 current = next as Record<string, unknown>;
32 }
33
34 current[lastKey] = value;
35};
36
37// Helper function to find any item in the store by its ID
38export const findItemInStore = (
39 state: ScenarioState,
40 id: string | null
41): ScenarioItem | null => {
42 if (!id) return null;
43 if (id === 'global-parameters') return state.globalParameters;
44
45 const collections = [
46 state.waveforms,
47 state.timings,
48 state.antennas,
49 state.platforms,
50 ];
51 for (const collection of collections) {
52 const item = collection.find((i) => i.id === id);
53 if (item) return item as ScenarioItem;
54 }
55 return null;
56};
57
58// Helper to find a platform component and its parent platform by component ID
59export const findComponentInStore = (
60 state: ScenarioState,
61 componentId: string | null
62): { platform: Platform; component: PlatformComponent } | null => {
63 if (!componentId) return null;
64 for (const platform of state.platforms) {
65 const component = platform.components.find((c) => c.id === componentId);
66 if (component) {
67 return { platform, component };
68 }
69 }
70 return null;
71};
72
73export function angleUnitToRadians(
74 value: number,
75 unit: GlobalParameters['rotationAngleUnit']
76): number {
77 return unit === 'deg' ? (value * Math.PI) / 180 : value;
78}
79
80/**
81 * Calculates a platform's interpolated 3D position at a specific time.
82 * This function relies on the pre-fetched `pathPoints` array stored on the platform object.
83 * @param {Platform} platform The platform data, including its waypoints and cached path points.
84 * @param {number} currentTime The global simulation time.
85 * @returns {Vector3} The interpolated position in Three.js coordinates.
86 */
87export function calculateInterpolatedPosition(
88 platform: Platform,
89 currentTime: number
90): Vector3 {
91 const { waypoints, interpolation } = platform.motionPath;
92 const pathPoints = platform.pathPoints ?? [];
93
94 const firstWaypoint = waypoints[0];
95 if (!firstWaypoint) return new Vector3(0, 0, 0);
96
97 const staticPosition = new Vector3(
98 firstWaypoint.x ?? 0,
99 firstWaypoint.altitude ?? 0,
100 -(firstWaypoint.y ?? 0)
101 );
102
103 if (
104 interpolation === 'static' ||
105 waypoints.length < 2 ||
106 pathPoints.length < 2
107 ) {
108 return staticPosition;
109 }
110
111 const lastWaypoint = waypoints[waypoints.length - 1];
112 const pathStartTime = firstWaypoint.time;
113 const pathEndTime = lastWaypoint.time;
114 const pathDuration = pathEndTime - pathStartTime;
115
116 if (pathDuration <= 0) return staticPosition;
117
118 const timeRatio = (currentTime - pathStartTime) / pathDuration;
119 const clampedRatio = Math.max(0, Math.min(1, timeRatio));
120
121 const floatIndex = clampedRatio * (pathPoints.length - 1);
122 const index1 = Math.floor(floatIndex);
123 const index2 = Math.min(pathPoints.length - 1, Math.ceil(floatIndex));
124
125 const point1 = pathPoints[index1];
126 const point2 = pathPoints[index2];
127
128 if (!point1 || !point2) return staticPosition;
129
130 const v1 = new Vector3(point1.x, point1.y, point1.z);
131 if (index1 === index2) return v1;
132
133 const v2 = new Vector3(point2.x, point2.y, point2.z);
134 const interPointRatio = floatIndex - index1;
135 return v1.clone().lerp(v2, interPointRatio);
136}
137
138/**
139 * Calculates a platform's interpolated velocity vector at a specific time.
140 * @param {Platform} platform The platform data.
141 * @param {number} currentTime The global simulation time.
142 * @returns {Vector3} The interpolated velocity in Three.js coordinates.
143 */
144export function calculateInterpolatedVelocity(
145 platform: Platform,
146 currentTime: number
147): Vector3 {
148 const { waypoints, interpolation } = platform.motionPath;
149 const pathPoints = platform.pathPoints ?? [];
150 const firstWaypoint = waypoints[0];
151
152 if (
153 !firstWaypoint ||
154 interpolation === 'static' ||
155 waypoints.length < 2 ||
156 pathPoints.length < 2
157 ) {
158 return new Vector3(0, 0, 0);
159 }
160
161 const lastWaypoint = waypoints[waypoints.length - 1];
162 const pathStartTime = firstWaypoint.time;
163 const pathEndTime = lastWaypoint.time;
164 const pathDuration = pathEndTime - pathStartTime;
165
166 if (pathDuration <= 0) return new Vector3(0, 0, 0);
167
168 const timeRatio = (currentTime - pathStartTime) / pathDuration;
169 const clampedRatio = Math.max(0, Math.min(1, timeRatio));
170 const floatIndex = clampedRatio * (pathPoints.length - 1);
171 const index1 = Math.floor(floatIndex);
172 const index2 = Math.min(pathPoints.length - 1, Math.ceil(floatIndex));
173
174 const p1 = pathPoints[index1];
175 const p2 = pathPoints[index2];
176
177 if (!p1 || !p2) return new Vector3(0, 0, 0);
178 if (index1 === index2) return new Vector3(p1.vx, p1.vy, p1.vz);
179
180 const interPointRatio = floatIndex - index1;
181 const v1 = new Vector3(p1.vx, p1.vy, p1.vz);
182 const v2 = new Vector3(p2.vx, p2.vy, p2.vz);
183 return v1.lerp(v2, interPointRatio);
184}
185
186/**
187 * Calculates a platform's interpolated rotation (Euler) at a specific time.
188 * @param {Platform} platform The platform data.
189 * @param {number} currentTime The global simulation time.
190 * @returns {Euler} The interpolated rotation in Three.js coordinates (YXZ order).
191 */
192export function calculateInterpolatedRotation(
193 platform: Platform,
194 currentTime: number,
195 angleUnit: GlobalParameters['rotationAngleUnit']
196): Euler {
197 const { rotation } = platform;
198 let azDeg = 0;
199 let elDeg = 0;
200
201 if (rotation.type === 'fixed') {
202 // Linear calculation based on rate
203 const dt = Math.max(0, currentTime); // Assume t=0 start for fixed
204 azDeg = rotation.startAzimuth + rotation.azimuthRate * dt;
205 elDeg = rotation.startElevation + rotation.elevationRate * dt;
206 } else {
207 // Path based interpolation
208 const waypoints = rotation.waypoints;
209 const pathPoints = platform.rotationPathPoints ?? [];
210 const firstWaypoint = waypoints[0];
211
212 if (!firstWaypoint) return new Euler(0, 0, 0);
213
214 // Default to start
215 azDeg = firstWaypoint.azimuth;
216 elDeg = firstWaypoint.elevation;
217
218 if (
219 rotation.interpolation !== 'static' &&
220 waypoints.length >= 2 &&
221 pathPoints.length >= 2
222 ) {
223 const lastWaypoint = waypoints[waypoints.length - 1];
224 const pathStartTime = firstWaypoint.time;
225 const pathDuration = lastWaypoint.time - pathStartTime;
226
227 if (pathDuration > 0) {
228 const timeRatio = (currentTime - pathStartTime) / pathDuration;
229 const clampedRatio = Math.max(0, Math.min(1, timeRatio));
230 const floatIndex = clampedRatio * (pathPoints.length - 1);
231 const index1 = Math.floor(floatIndex);
232 const index2 = Math.min(
233 pathPoints.length - 1,
234 Math.ceil(floatIndex)
235 );
236
237 const p1 = pathPoints[index1];
238 const p2 = pathPoints[index2];
239
240 if (p1 && p2) {
241 const t = floatIndex - index1;
242 // Simple linear interpolation of angles for visualization
243 azDeg = p1.azimuth + (p2.azimuth - p1.azimuth) * t;
244 elDeg = p1.elevation + (p2.elevation - p1.elevation) * t;
245 }
246 }
247 }
248 }
249
250 // Convert Compass Degrees (0 is North, CW) to Three.js Radians (0 is -Z?, CCW?)
251 // FERS: 0 Az = North (Y), 90 Az = East (X).
252 // Three.js: Y is Up.
253 // We apply Azimuth as rotation around Y.
254 // We apply Elevation as rotation around X.
255
256 // Convert deg to rad
257 const azRad = -angleUnitToRadians(azDeg, angleUnit); // Negate for CCW rotation in Three.js vs CW compass
258 const elRad = angleUnitToRadians(elDeg, angleUnit);
259
260 // Order YXZ: Rotate Azimuth (Y) first, then Elevation (X) (Pitch)
261 return new Euler(elRad, azRad, 0, 'YXZ');
262}