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';
8import { createUniqueScenarioName } from '../nameUtils';
11 serializeComponentInner,
13} from '../serializers';
15 enqueueFullSyncDetached,
16 enqueueGranularSyncDetached,
24import { buildScenarioJson } from './backendSlice';
26const NUM_PATH_POINTS = 100;
27type InterpolationType = 'static' | 'linear' | 'cubic';
28interface InterpolatedPoint {
37interface InterpolatedRotationPoint {
42function syncPlatformGranular(
43 getFn: () => ScenarioStore,
46 const platform = getFn().platforms.find((p) => p.id === platformId);
47 if (!platform) return;
48 enqueueGranularSyncDetached(
51 JSON.stringify(cleanObject(serializePlatform(platform)))
55export const createPlatformSlice: StateCreator<
57 [['zustand/immer', never]],
63 const id = generateSimId('Platform');
64 const newName = `Platform ${state.platforms.length + 1}`;
65 const newPlatform: Platform = {
66 ...createDefaultPlatform(),
68 name: createUniqueScenarioName(state, newName),
70 // Defaults to empty components list
71 state.platforms.push(newPlatform);
74 // libfers has no granular add API for Platforms — full sync is required.
75 enqueueFullSyncDetached(() => buildScenarioJson(get()));
77 addPositionWaypoint: (platformId) => {
80 const platform = state.platforms.find((p) => p.id === platformId);
82 platform.motionPath.waypoints.push({
83 id: generateSimId('Platform'),
93 if (touched) syncPlatformGranular(get, platformId);
95 removePositionWaypoint: (platformId, waypointId) => {
98 const platform = state.platforms.find((p) => p.id === platformId);
99 if (platform && platform.motionPath.waypoints.length > 1) {
100 const index = platform.motionPath.waypoints.findIndex(
101 (wp) => wp.id === waypointId
104 platform.motionPath.waypoints.splice(index, 1);
105 state.isDirty = true;
110 if (touched) syncPlatformGranular(get, platformId);
112 addRotationWaypoint: (platformId) => {
115 const platform = state.platforms.find((p) => p.id === platformId);
116 if (platform?.rotation.type === 'path') {
117 platform.rotation.waypoints.push({
118 id: generateSimId('Platform'),
123 state.isDirty = true;
127 if (touched) syncPlatformGranular(get, platformId);
129 removeRotationWaypoint: (platformId, waypointId) => {
132 const platform = state.platforms.find((p) => p.id === platformId);
134 platform?.rotation.type === 'path' &&
135 platform.rotation.waypoints.length > 1
137 const index = platform.rotation.waypoints.findIndex(
138 (wp) => wp.id === waypointId
141 platform.rotation.waypoints.splice(index, 1);
142 state.isDirty = true;
147 if (touched) syncPlatformGranular(get, platformId);
149 addPlatformComponent: (platformId, componentType) => {
152 const platform = state.platforms.find((p) => p.id === platformId);
153 if (!platform) return;
156 componentType === 'transmitter'
157 ? generateSimId('Transmitter')
158 : componentType === 'receiver'
159 ? generateSimId('Receiver')
160 : componentType === 'target'
161 ? generateSimId('Target')
162 : generateSimId('Transmitter');
164 componentType === 'monostatic'
165 ? generateSimId('Receiver')
167 const name = `${platform.name} ${
168 componentType.charAt(0).toUpperCase() + componentType.slice(1)
170 const uniqueName = createUniqueScenarioName(state, name);
171 let newComponent: PlatformComponent;
173 switch (componentType) {
179 rxId: rxId ?? generateSimId('Receiver'),
188 noiseTemperature: 290,
189 noDirectPaths: false,
190 noPropagationLoss: false,
218 noiseTemperature: 290,
219 noDirectPaths: false,
220 noPropagationLoss: false,
229 rcs_type: 'isotropic',
231 rcs_model: 'constant',
237 platform.components.push(newComponent);
238 state.isDirty = true;
242 // Components (Transmitter/Receiver/Target/Monostatic) are independent
243 // backend objects with their own IDs; there is no granular add API.
244 enqueueFullSyncDetached(() => buildScenarioJson(get()));
247 removePlatformComponent: (platformId, componentId) => {
250 const platform = state.platforms.find((p) => p.id === platformId);
252 const index = platform.components.findIndex(
253 (c) => c.id === componentId
256 platform.components.splice(index, 1);
257 if (state.selectedComponentId === componentId) {
258 state.selectedComponentId = null;
260 state.isDirty = true;
266 // libfers has no granular remove API — full sync is required.
267 enqueueFullSyncDetached(() => buildScenarioJson(get()));
270 setPlatformRcsModel: (platformId, componentId, newModel) => {
273 const platform = state.platforms.find((p) => p.id === platformId);
274 const component = platform?.components.find(
275 (c) => c.id === componentId
277 if (component?.type === 'target') {
278 component.rcs_model = newModel;
279 if (newModel === 'chisquare' || newModel === 'gamma') {
280 if (typeof component.rcs_k !== 'number') {
281 component.rcs_k = 1.0;
284 delete component.rcs_k;
286 state.isDirty = true;
291 const platform = get().platforms.find((p) => p.id === platformId);
292 const component = platform?.components.find(
293 (c) => c.id === componentId
296 enqueueGranularSyncDetached(
300 cleanObject(serializeComponentInner(component))
306 fetchPlatformPath: async (platformId) => {
307 const { platforms, showError, globalParameters } = get();
308 const platform = platforms.find((p) => p.id === platformId);
310 if (!platform) return;
312 // 1. Fetch/Calculate Motion Path
313 const { waypoints, interpolation } = platform.motionPath;
324 if (waypoints.length < 2 || interpolation === 'static') {
325 // Static or single point: Calculate directly on frontend (velocity 0)
326 newPathPoints = waypoints.map((wp) => ({
329 z: -wp.y, // ENU Y -> Three JS -Z
335 // Dynamic: Fetch interpolated points from Backend
336 const points = await invoke<InterpolatedPoint[]>(
337 'get_interpolated_motion_path',
340 interpType: interpolation as InterpolationType,
341 numPoints: NUM_PATH_POINTS,
344 // Convert ENU (Backend) to Three.js coordinates
345 // Pos: X->X, Alt->Y, Y->-Z
346 // Vel: Vx->Vx, Vz->Vy, Vy->-Vz
347 newPathPoints = points.map((p) => ({
357 const msg = error instanceof Error ? error.message : String(error);
359 `Failed to fetch motion path for ${platform.name}:`,
362 showError(`Failed to get motion path for ${platform.name}: ${msg}`);
363 // Fallback to empty to prevent stale data
367 // 2. Fetch/Calculate Rotation Path
368 const { rotation } = platform;
369 let newRotationPoints:
370 | { azimuth: number; elevation: number }[]
371 | undefined = undefined;
373 if (rotation.type === 'path') {
374 const rotWaypoints = rotation.waypoints;
375 // Only fetch from backend if dynamic. Static/Single points are handled by the real-time calculator.
377 rotWaypoints.length >= 2 &&
378 rotation.interpolation !== 'static'
381 const points = await invoke<InterpolatedRotationPoint[]>(
382 'get_interpolated_rotation_path',
384 waypoints: rotWaypoints,
386 rotation.interpolation as InterpolationType,
387 angleUnit: globalParameters.rotationAngleUnit,
388 numPoints: NUM_PATH_POINTS,
391 newRotationPoints = points.map((p) => ({
393 elevation: p.elevation,
397 error instanceof Error ? error.message : String(error);
399 `Failed to fetch rotation path for ${platform.name}:`,
402 // Log error but don't break the whole update; standard calc will fallback to first waypoint
409 const p = state.platforms.find((p) => p.id === platformId);
411 p.pathPoints = newPathPoints;
412 p.rotationPathPoints = newRotationPoints;