1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
4import { Euler, Vector3 } from 'three';
13// Helper to set nested properties safely
14export const setPropertyByPath = (
19 const keys = path.split('.');
20 const lastKey = keys.pop();
23 let current: Record<string, unknown> = obj as Record<string, unknown>;
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.
31 current = next as Record<string, unknown>;
34 current[lastKey] = value;
37// Helper function to find any item in the store by its ID
38export const findItemInStore = (
41): ScenarioItem | null => {
43 if (id === 'global-parameters') return state.globalParameters;
51 for (const collection of collections) {
52 const item = collection.find((i) => i.id === id);
53 if (item) return item as ScenarioItem;
58// Helper to find a platform component and its parent platform by component ID
59export const findComponentInStore = (
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);
67 return { platform, component };
73export function angleUnitToRadians(
75 unit: GlobalParameters['rotationAngleUnit']
77 return unit === 'deg' ? (value * Math.PI) / 180 : value;
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.
87export function calculateInterpolatedPosition(
91 const { waypoints, interpolation } = platform.motionPath;
92 const pathPoints = platform.pathPoints ?? [];
94 const firstWaypoint = waypoints[0];
95 if (!firstWaypoint) return new Vector3(0, 0, 0);
97 const staticPosition = new Vector3(
99 firstWaypoint.altitude ?? 0,
100 -(firstWaypoint.y ?? 0)
104 interpolation === 'static' ||
105 waypoints.length < 2 ||
106 pathPoints.length < 2
108 return staticPosition;
111 const lastWaypoint = waypoints[waypoints.length - 1];
112 const pathStartTime = firstWaypoint.time;
113 const pathEndTime = lastWaypoint.time;
114 const pathDuration = pathEndTime - pathStartTime;
116 if (pathDuration <= 0) return staticPosition;
118 const timeRatio = (currentTime - pathStartTime) / pathDuration;
119 const clampedRatio = Math.max(0, Math.min(1, timeRatio));
121 const floatIndex = clampedRatio * (pathPoints.length - 1);
122 const index1 = Math.floor(floatIndex);
123 const index2 = Math.min(pathPoints.length - 1, Math.ceil(floatIndex));
125 const point1 = pathPoints[index1];
126 const point2 = pathPoints[index2];
128 if (!point1 || !point2) return staticPosition;
130 const v1 = new Vector3(point1.x, point1.y, point1.z);
131 if (index1 === index2) return v1;
133 const v2 = new Vector3(point2.x, point2.y, point2.z);
134 const interPointRatio = floatIndex - index1;
135 return v1.clone().lerp(v2, interPointRatio);
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.
144export function calculateInterpolatedVelocity(
148 const { waypoints, interpolation } = platform.motionPath;
149 const pathPoints = platform.pathPoints ?? [];
150 const firstWaypoint = waypoints[0];
154 interpolation === 'static' ||
155 waypoints.length < 2 ||
156 pathPoints.length < 2
158 return new Vector3(0, 0, 0);
161 const lastWaypoint = waypoints[waypoints.length - 1];
162 const pathStartTime = firstWaypoint.time;
163 const pathEndTime = lastWaypoint.time;
164 const pathDuration = pathEndTime - pathStartTime;
166 if (pathDuration <= 0) return new Vector3(0, 0, 0);
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));
174 const p1 = pathPoints[index1];
175 const p2 = pathPoints[index2];
177 if (!p1 || !p2) return new Vector3(0, 0, 0);
178 if (index1 === index2) return new Vector3(p1.vx, p1.vy, p1.vz);
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);
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).
192export function calculateInterpolatedRotation(
195 angleUnit: GlobalParameters['rotationAngleUnit']
197 const { rotation } = platform;
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;
207 // Path based interpolation
208 const waypoints = rotation.waypoints;
209 const pathPoints = platform.rotationPathPoints ?? [];
210 const firstWaypoint = waypoints[0];
212 if (!firstWaypoint) return new Euler(0, 0, 0);
215 azDeg = firstWaypoint.azimuth;
216 elDeg = firstWaypoint.elevation;
219 rotation.interpolation !== 'static' &&
220 waypoints.length >= 2 &&
221 pathPoints.length >= 2
223 const lastWaypoint = waypoints[waypoints.length - 1];
224 const pathStartTime = firstWaypoint.time;
225 const pathDuration = lastWaypoint.time - pathStartTime;
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)
237 const p1 = pathPoints[index1];
238 const p2 = pathPoints[index2];
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;
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.
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);
260 // Order YXZ: Rotate Azimuth (Y) first, then Elevation (X) (Pitch)
261 return new Euler(elRad, azRad, 0, 'YXZ');