1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
4import { invoke } from '@tauri-apps/api/core';
5import { StateCreator } from 'zustand';
6import { createDefaultPlatform } from '../defaults';
7import { generateSimId } from '../idUtils';
10 serializeComponentInner,
12} from '../serializers';
13import { enqueueFullSync, enqueueGranularSync } from '../syncQueue';
20import { buildScenarioJson } from './backendSlice';
22const NUM_PATH_POINTS = 100;
23type InterpolationType = 'static' | 'linear' | 'cubic';
24interface InterpolatedPoint {
33interface InterpolatedRotationPoint {
38function syncPlatformGranular(
39 getFn: () => ScenarioStore,
42 const platform = getFn().platforms.find((p) => p.id === platformId);
43 if (!platform) return;
44 void enqueueGranularSync(
47 JSON.stringify(cleanObject(serializePlatform(platform)))
51export const createPlatformSlice: StateCreator<
53 [['zustand/immer', never]],
59 const id = generateSimId('Platform');
60 const newName = `Platform ${state.platforms.length + 1}`;
61 const newPlatform: Platform = {
62 ...createDefaultPlatform(),
66 // Defaults to empty components list
67 state.platforms.push(newPlatform);
70 // libfers has no granular add API for Platforms — full sync is required.
71 void enqueueFullSync(() => buildScenarioJson(get()));
73 addPositionWaypoint: (platformId) => {
76 const platform = state.platforms.find((p) => p.id === platformId);
78 platform.motionPath.waypoints.push({
79 id: generateSimId('Platform'),
89 if (touched) syncPlatformGranular(get, platformId);
91 removePositionWaypoint: (platformId, waypointId) => {
94 const platform = state.platforms.find((p) => p.id === platformId);
95 if (platform && platform.motionPath.waypoints.length > 1) {
96 const index = platform.motionPath.waypoints.findIndex(
97 (wp) => wp.id === waypointId
100 platform.motionPath.waypoints.splice(index, 1);
101 state.isDirty = true;
106 if (touched) syncPlatformGranular(get, platformId);
108 addRotationWaypoint: (platformId) => {
111 const platform = state.platforms.find((p) => p.id === platformId);
112 if (platform?.rotation.type === 'path') {
113 platform.rotation.waypoints.push({
114 id: generateSimId('Platform'),
119 state.isDirty = true;
123 if (touched) syncPlatformGranular(get, platformId);
125 removeRotationWaypoint: (platformId, waypointId) => {
128 const platform = state.platforms.find((p) => p.id === platformId);
130 platform?.rotation.type === 'path' &&
131 platform.rotation.waypoints.length > 1
133 const index = platform.rotation.waypoints.findIndex(
134 (wp) => wp.id === waypointId
137 platform.rotation.waypoints.splice(index, 1);
138 state.isDirty = true;
143 if (touched) syncPlatformGranular(get, platformId);
145 addPlatformComponent: (platformId, componentType) => {
148 const platform = state.platforms.find((p) => p.id === platformId);
149 if (!platform) return;
152 componentType === 'transmitter'
153 ? generateSimId('Transmitter')
154 : componentType === 'receiver'
155 ? generateSimId('Receiver')
156 : componentType === 'target'
157 ? generateSimId('Target')
158 : generateSimId('Transmitter');
160 componentType === 'monostatic'
161 ? generateSimId('Receiver')
163 const name = `${platform.name} ${
164 componentType.charAt(0).toUpperCase() + componentType.slice(1)
166 let newComponent: PlatformComponent;
168 switch (componentType) {
174 rxId: rxId ?? generateSimId('Receiver'),
183 noiseTemperature: 290,
184 noDirectPaths: false,
185 noPropagationLoss: false,
213 noiseTemperature: 290,
214 noDirectPaths: false,
215 noPropagationLoss: false,
224 rcs_type: 'isotropic',
226 rcs_model: 'constant',
232 platform.components.push(newComponent);
233 state.isDirty = true;
237 // Components (Transmitter/Receiver/Target/Monostatic) are independent
238 // backend objects with their own IDs; there is no granular add API.
239 void enqueueFullSync(() => buildScenarioJson(get()));
242 removePlatformComponent: (platformId, componentId) => {
245 const platform = state.platforms.find((p) => p.id === platformId);
247 const index = platform.components.findIndex(
248 (c) => c.id === componentId
251 platform.components.splice(index, 1);
252 if (state.selectedComponentId === componentId) {
253 state.selectedComponentId = null;
255 state.isDirty = true;
261 // libfers has no granular remove API — full sync is required.
262 void enqueueFullSync(() => buildScenarioJson(get()));
265 setPlatformRcsModel: (platformId, componentId, newModel) => {
268 const platform = state.platforms.find((p) => p.id === platformId);
269 const component = platform?.components.find(
270 (c) => c.id === componentId
272 if (component?.type === 'target') {
273 component.rcs_model = newModel;
274 if (newModel === 'chisquare' || newModel === 'gamma') {
275 if (typeof component.rcs_k !== 'number') {
276 component.rcs_k = 1.0;
279 delete component.rcs_k;
281 state.isDirty = true;
286 const platform = get().platforms.find((p) => p.id === platformId);
287 const component = platform?.components.find(
288 (c) => c.id === componentId
291 void enqueueGranularSync(
295 cleanObject(serializeComponentInner(component))
301 fetchPlatformPath: async (platformId) => {
302 const { platforms, showError, globalParameters } = get();
303 const platform = platforms.find((p) => p.id === platformId);
305 if (!platform) return;
307 // 1. Fetch/Calculate Motion Path
308 const { waypoints, interpolation } = platform.motionPath;
319 if (waypoints.length < 2 || interpolation === 'static') {
320 // Static or single point: Calculate directly on frontend (velocity 0)
321 newPathPoints = waypoints.map((wp) => ({
324 z: -wp.y, // ENU Y -> Three JS -Z
330 // Dynamic: Fetch interpolated points from Backend
331 const points = await invoke<InterpolatedPoint[]>(
332 'get_interpolated_motion_path',
335 interpType: interpolation as InterpolationType,
336 numPoints: NUM_PATH_POINTS,
339 // Convert ENU (Backend) to Three.js coordinates
340 // Pos: X->X, Alt->Y, Y->-Z
341 // Vel: Vx->Vx, Vz->Vy, Vy->-Vz
342 newPathPoints = points.map((p) => ({
352 const msg = error instanceof Error ? error.message : String(error);
354 `Failed to fetch motion path for ${platform.name}:`,
357 showError(`Failed to get motion path for ${platform.name}: ${msg}`);
358 // Fallback to empty to prevent stale data
362 // 2. Fetch/Calculate Rotation Path
363 const { rotation } = platform;
364 let newRotationPoints:
365 | { azimuth: number; elevation: number }[]
366 | undefined = undefined;
368 if (rotation.type === 'path') {
369 const rotWaypoints = rotation.waypoints;
370 // Only fetch from backend if dynamic. Static/Single points are handled by the real-time calculator.
372 rotWaypoints.length >= 2 &&
373 rotation.interpolation !== 'static'
376 const points = await invoke<InterpolatedRotationPoint[]>(
377 'get_interpolated_rotation_path',
379 waypoints: rotWaypoints,
381 rotation.interpolation as InterpolationType,
382 angleUnit: globalParameters.rotationAngleUnit,
383 numPoints: NUM_PATH_POINTS,
386 newRotationPoints = points.map((p) => ({
388 elevation: p.elevation,
392 error instanceof Error ? error.message : String(error);
394 `Failed to fetch rotation path for ${platform.name}:`,
397 // Log error but don't break the whole update; standard calc will fallback to first waypoint
404 const p = state.platforms.find((p) => p.id === platformId);
406 p.pathPoints = newPathPoints;
407 p.rotationPathPoints = newRotationPoints;