1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
4import { StateCreator } from 'zustand';
5import { v4 as uuidv4 } from 'uuid';
6import { invoke } from '@tauri-apps/api/core';
13import { createDefaultPlatform } from '../defaults';
15const NUM_PATH_POINTS = 100;
16type InterpolationType = 'static' | 'linear' | 'cubic';
17interface InterpolatedPoint {
26interface InterpolatedRotationPoint {
28 elevation_deg: number;
31export const createPlatformSlice: StateCreator<
33 [['zustand/immer', never]],
40 const newName = `Platform ${state.platforms.length + 1}`;
41 const newPlatform: Platform = {
42 ...createDefaultPlatform(),
46 // Defaults to empty components list
47 state.platforms.push(newPlatform);
50 addPositionWaypoint: (platformId) =>
52 const platform = state.platforms.find((p) => p.id === platformId);
54 platform.motionPath.waypoints.push({
64 removePositionWaypoint: (platformId, waypointId) =>
66 const platform = state.platforms.find((p) => p.id === platformId);
67 if (platform && platform.motionPath.waypoints.length > 1) {
68 const index = platform.motionPath.waypoints.findIndex(
69 (wp) => wp.id === waypointId
72 platform.motionPath.waypoints.splice(index, 1);
77 addRotationWaypoint: (platformId) =>
79 const platform = state.platforms.find((p) => p.id === platformId);
80 if (platform?.rotation.type === 'path') {
81 platform.rotation.waypoints.push({
90 removeRotationWaypoint: (platformId, waypointId) =>
92 const platform = state.platforms.find((p) => p.id === platformId);
94 platform?.rotation.type === 'path' &&
95 platform.rotation.waypoints.length > 1
97 const index = platform.rotation.waypoints.findIndex(
98 (wp) => wp.id === waypointId
101 platform.rotation.waypoints.splice(index, 1);
102 state.isDirty = true;
106 addPlatformComponent: (platformId, componentType) =>
108 const platform = state.platforms.find((p) => p.id === platformId);
109 if (!platform) return;
112 const name = `${platform.name} ${
113 componentType.charAt(0).toUpperCase() + componentType.slice(1)
115 let newComponent: PlatformComponent;
117 switch (componentType) {
130 noiseTemperature: 290,
131 noDirectPaths: false,
132 noPropagationLoss: false,
160 noiseTemperature: 290,
161 noDirectPaths: false,
162 noPropagationLoss: false,
171 rcs_type: 'isotropic',
173 rcs_model: 'constant',
179 platform.components.push(newComponent);
180 state.isDirty = true;
182 removePlatformComponent: (platformId, componentId) =>
184 const platform = state.platforms.find((p) => p.id === platformId);
186 const index = platform.components.findIndex(
187 (c) => c.id === componentId
190 platform.components.splice(index, 1);
191 state.isDirty = true;
195 setPlatformRcsModel: (platformId, componentId, newModel) =>
197 const platform = state.platforms.find((p) => p.id === platformId);
198 const component = platform?.components.find(
199 (c) => c.id === componentId
201 if (component?.type === 'target') {
202 component.rcs_model = newModel;
203 if (newModel === 'chisquare' || newModel === 'gamma') {
204 if (typeof component.rcs_k !== 'number') {
205 component.rcs_k = 1.0;
208 delete component.rcs_k;
210 state.isDirty = true;
213 fetchPlatformPath: async (platformId) => {
214 const { platforms, showError } = get();
215 const platform = platforms.find((p) => p.id === platformId);
217 if (!platform) return;
219 // 1. Fetch/Calculate Motion Path
220 const { waypoints, interpolation } = platform.motionPath;
231 if (waypoints.length < 2 || interpolation === 'static') {
232 // Static or single point: Calculate directly on frontend (velocity 0)
233 newPathPoints = waypoints.map((wp) => ({
236 z: -wp.y, // ENU Y -> Three JS -Z
242 // Dynamic: Fetch interpolated points from Backend
243 const points = await invoke<InterpolatedPoint[]>(
244 'get_interpolated_motion_path',
247 interpType: interpolation as InterpolationType,
248 numPoints: NUM_PATH_POINTS,
251 // Convert ENU (Backend) to Three.js coordinates
252 // Pos: X->X, Alt->Y, Y->-Z
253 // Vel: Vx->Vx, Vz->Vy, Vy->-Vz
254 newPathPoints = points.map((p) => ({
264 const msg = error instanceof Error ? error.message : String(error);
266 `Failed to fetch motion path for ${platform.name}:`,
269 showError(`Failed to get motion path for ${platform.name}: ${msg}`);
270 // Fallback to empty to prevent stale data
274 // 2. Fetch/Calculate Rotation Path
275 const { rotation } = platform;
276 let newRotationPoints:
277 | { azimuth: number; elevation: number }[]
278 | undefined = undefined;
280 if (rotation.type === 'path') {
281 const rotWaypoints = rotation.waypoints;
282 // Only fetch from backend if dynamic. Static/Single points are handled by the real-time calculator.
284 rotWaypoints.length >= 2 &&
285 rotation.interpolation !== 'static'
288 const points = await invoke<InterpolatedRotationPoint[]>(
289 'get_interpolated_rotation_path',
291 waypoints: rotWaypoints,
293 rotation.interpolation as InterpolationType,
294 numPoints: NUM_PATH_POINTS,
297 newRotationPoints = points.map((p) => ({
298 azimuth: p.azimuth_deg,
299 elevation: p.elevation_deg,
303 error instanceof Error ? error.message : String(error);
305 `Failed to fetch rotation path for ${platform.name}:`,
308 // Log error but don't break the whole update; standard calc will fallback to first waypoint
315 const p = state.platforms.find((p) => p.id === platformId);
317 p.pathPoints = newPathPoints;
318 p.rotationPathPoints = newRotationPoints;