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