1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
4import { ScenarioState, ScenarioItem, Platform } from './types';
5import { Euler, Vector3 } from 'three';
7// Helper to set nested properties safely
8export const setPropertyByPath = (
13 const keys = path.split('.');
14 const lastKey = keys.pop();
17 let current: Record<string, unknown> = obj as Record<string, unknown>;
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.
25 current = next as Record<string, unknown>;
28 current[lastKey] = value;
31// Helper function to find any item in the store by its ID
32export const findItemInStore = (
35): ScenarioItem | null => {
37 if (id === 'global-parameters') return state.globalParameters;
45 for (const collection of collections) {
46 const item = collection.find((i) => i.id === id);
47 if (item) return item as ScenarioItem;
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.
59export function calculateInterpolatedPosition(
63 const { waypoints, interpolation } = platform.motionPath;
64 const pathPoints = platform.pathPoints ?? [];
66 const firstWaypoint = waypoints[0];
67 if (!firstWaypoint) return new Vector3(0, 0, 0);
69 const staticPosition = new Vector3(
71 firstWaypoint.altitude ?? 0,
72 -(firstWaypoint.y ?? 0)
76 interpolation === 'static' ||
77 waypoints.length < 2 ||
80 return staticPosition;
83 const lastWaypoint = waypoints[waypoints.length - 1];
84 const pathStartTime = firstWaypoint.time;
85 const pathEndTime = lastWaypoint.time;
86 const pathDuration = pathEndTime - pathStartTime;
88 if (pathDuration <= 0) return staticPosition;
90 const timeRatio = (currentTime - pathStartTime) / pathDuration;
91 const clampedRatio = Math.max(0, Math.min(1, timeRatio));
93 const floatIndex = clampedRatio * (pathPoints.length - 1);
94 const index1 = Math.floor(floatIndex);
95 const index2 = Math.min(pathPoints.length - 1, Math.ceil(floatIndex));
97 const point1 = pathPoints[index1];
98 const point2 = pathPoints[index2];
100 if (!point1 || !point2) return staticPosition;
102 const v1 = new Vector3(point1.x, point1.y, point1.z);
103 if (index1 === index2) return v1;
105 const v2 = new Vector3(point2.x, point2.y, point2.z);
106 const interPointRatio = floatIndex - index1;
107 return v1.clone().lerp(v2, interPointRatio);
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.
116export function calculateInterpolatedVelocity(
120 const { waypoints, interpolation } = platform.motionPath;
121 const pathPoints = platform.pathPoints ?? [];
122 const firstWaypoint = waypoints[0];
126 interpolation === 'static' ||
127 waypoints.length < 2 ||
128 pathPoints.length < 2
130 return new Vector3(0, 0, 0);
133 const lastWaypoint = waypoints[waypoints.length - 1];
134 const pathStartTime = firstWaypoint.time;
135 const pathEndTime = lastWaypoint.time;
136 const pathDuration = pathEndTime - pathStartTime;
138 if (pathDuration <= 0) return new Vector3(0, 0, 0);
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));
146 const p1 = pathPoints[index1];
147 const p2 = pathPoints[index2];
149 if (!p1 || !p2) return new Vector3(0, 0, 0);
150 if (index1 === index2) return new Vector3(p1.vx, p1.vy, p1.vz);
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);
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).
164export function calculateInterpolatedRotation(
168 const { rotation } = platform;
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;
178 // Path based interpolation
179 const waypoints = rotation.waypoints;
180 const pathPoints = platform.rotationPathPoints ?? [];
181 const firstWaypoint = waypoints[0];
183 if (!firstWaypoint) return new Euler(0, 0, 0);
186 azDeg = firstWaypoint.azimuth;
187 elDeg = firstWaypoint.elevation;
190 rotation.interpolation !== 'static' &&
191 waypoints.length >= 2 &&
192 pathPoints.length >= 2
194 const lastWaypoint = waypoints[waypoints.length - 1];
195 const pathStartTime = firstWaypoint.time;
196 const pathDuration = lastWaypoint.time - pathStartTime;
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)
208 const p1 = pathPoints[index1];
209 const p2 = pathPoints[index2];
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;
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.
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;
231 // Order YXZ: Rotate Azimuth (Y) first, then Elevation (X) (Pitch)
232 return new Euler(elRad, azRad, 0, 'YXZ');