FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
platformSlice.ts
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
3
4import { invoke } from '@tauri-apps/api/core';
5import { StateCreator } from 'zustand';
6import { createDefaultPlatform } from '../defaults';
7import { generateSimId } from '../idUtils';
8import {
9 cleanObject,
10 serializeComponentInner,
11 serializePlatform,
12} from '../serializers';
13import { enqueueFullSync, enqueueGranularSync } from '../syncQueue';
14import {
15 Platform,
16 PlatformActions,
17 PlatformComponent,
18 ScenarioStore,
19} from '../types';
20import { buildScenarioJson } from './backendSlice';
21
22const NUM_PATH_POINTS = 100;
23type InterpolationType = 'static' | 'linear' | 'cubic';
24interface InterpolatedPoint {
25 x: number;
26 y: number;
27 z: number;
28 vx: number;
29 vy: number;
30 vz: number;
31}
32
33interface InterpolatedRotationPoint {
34 azimuth: number;
35 elevation: number;
36}
37
38function syncPlatformGranular(
39 getFn: () => ScenarioStore,
40 platformId: string
41): void {
42 const platform = getFn().platforms.find((p) => p.id === platformId);
43 if (!platform) return;
44 void enqueueGranularSync(
45 'Platform',
46 platform.id,
47 JSON.stringify(cleanObject(serializePlatform(platform)))
48 );
49}
50
51export const createPlatformSlice: StateCreator<
52 ScenarioStore,
53 [['zustand/immer', never]],
54 [],
55 PlatformActions
56> = (set, get) => ({
57 addPlatform: () => {
58 set((state) => {
59 const id = generateSimId('Platform');
60 const newName = `Platform ${state.platforms.length + 1}`;
61 const newPlatform: Platform = {
62 ...createDefaultPlatform(),
63 id,
64 name: newName,
65 };
66 // Defaults to empty components list
67 state.platforms.push(newPlatform);
68 state.isDirty = true;
69 });
70 // libfers has no granular add API for Platforms — full sync is required.
71 void enqueueFullSync(() => buildScenarioJson(get()));
72 },
73 addPositionWaypoint: (platformId) => {
74 let touched = false;
75 set((state) => {
76 const platform = state.platforms.find((p) => p.id === platformId);
77 if (platform) {
78 platform.motionPath.waypoints.push({
79 id: generateSimId('Platform'),
80 x: 0,
81 y: 0,
82 altitude: 0,
83 time: 0,
84 });
85 state.isDirty = true;
86 touched = true;
87 }
88 });
89 if (touched) syncPlatformGranular(get, platformId);
90 },
91 removePositionWaypoint: (platformId, waypointId) => {
92 let touched = false;
93 set((state) => {
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
98 );
99 if (index > -1) {
100 platform.motionPath.waypoints.splice(index, 1);
101 state.isDirty = true;
102 touched = true;
103 }
104 }
105 });
106 if (touched) syncPlatformGranular(get, platformId);
107 },
108 addRotationWaypoint: (platformId) => {
109 let touched = false;
110 set((state) => {
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'),
115 azimuth: 0,
116 elevation: 0,
117 time: 0,
118 });
119 state.isDirty = true;
120 touched = true;
121 }
122 });
123 if (touched) syncPlatformGranular(get, platformId);
124 },
125 removeRotationWaypoint: (platformId, waypointId) => {
126 let touched = false;
127 set((state) => {
128 const platform = state.platforms.find((p) => p.id === platformId);
129 if (
130 platform?.rotation.type === 'path' &&
131 platform.rotation.waypoints.length > 1
132 ) {
133 const index = platform.rotation.waypoints.findIndex(
134 (wp) => wp.id === waypointId
135 );
136 if (index > -1) {
137 platform.rotation.waypoints.splice(index, 1);
138 state.isDirty = true;
139 touched = true;
140 }
141 }
142 });
143 if (touched) syncPlatformGranular(get, platformId);
144 },
145 addPlatformComponent: (platformId, componentType) => {
146 let added = false;
147 set((state) => {
148 const platform = state.platforms.find((p) => p.id === platformId);
149 if (!platform) return;
150
151 const id =
152 componentType === 'transmitter'
153 ? generateSimId('Transmitter')
154 : componentType === 'receiver'
155 ? generateSimId('Receiver')
156 : componentType === 'target'
157 ? generateSimId('Target')
158 : generateSimId('Transmitter');
159 const rxId =
160 componentType === 'monostatic'
161 ? generateSimId('Receiver')
162 : null;
163 const name = `${platform.name} ${
164 componentType.charAt(0).toUpperCase() + componentType.slice(1)
165 }`;
166 let newComponent: PlatformComponent;
167
168 switch (componentType) {
169 case 'monostatic':
170 newComponent = {
171 id,
172 type: 'monostatic',
173 txId: id,
174 rxId: rxId ?? generateSimId('Receiver'),
175 name,
176 radarType: 'pulsed',
177 window_skip: 0,
178 window_length: 1e-5,
179 prf: 1000,
180 antennaId: null,
181 waveformId: null,
182 timingId: null,
183 noiseTemperature: 290,
184 noDirectPaths: false,
185 noPropagationLoss: false,
186 schedule: [],
187 };
188 break;
189 case 'transmitter':
190 newComponent = {
191 id,
192 type: 'transmitter',
193 name,
194 radarType: 'pulsed',
195 prf: 1000,
196 antennaId: null,
197 waveformId: null,
198 timingId: null,
199 schedule: [],
200 };
201 break;
202 case 'receiver':
203 newComponent = {
204 id,
205 type: 'receiver',
206 name,
207 radarType: 'pulsed',
208 window_skip: 0,
209 window_length: 1e-5,
210 prf: 1000,
211 antennaId: null,
212 timingId: null,
213 noiseTemperature: 290,
214 noDirectPaths: false,
215 noPropagationLoss: false,
216 schedule: [],
217 };
218 break;
219 case 'target':
220 newComponent = {
221 id,
222 type: 'target',
223 name,
224 rcs_type: 'isotropic',
225 rcs_value: 1,
226 rcs_model: 'constant',
227 };
228 break;
229 default:
230 return;
231 }
232 platform.components.push(newComponent);
233 state.isDirty = true;
234 added = true;
235 });
236 if (added) {
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()));
240 }
241 },
242 removePlatformComponent: (platformId, componentId) => {
243 let removed = false;
244 set((state) => {
245 const platform = state.platforms.find((p) => p.id === platformId);
246 if (platform) {
247 const index = platform.components.findIndex(
248 (c) => c.id === componentId
249 );
250 if (index > -1) {
251 platform.components.splice(index, 1);
252 if (state.selectedComponentId === componentId) {
253 state.selectedComponentId = null;
254 }
255 state.isDirty = true;
256 removed = true;
257 }
258 }
259 });
260 if (removed) {
261 // libfers has no granular remove API — full sync is required.
262 void enqueueFullSync(() => buildScenarioJson(get()));
263 }
264 },
265 setPlatformRcsModel: (platformId, componentId, newModel) => {
266 let touched = false;
267 set((state) => {
268 const platform = state.platforms.find((p) => p.id === platformId);
269 const component = platform?.components.find(
270 (c) => c.id === componentId
271 );
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;
277 }
278 } else {
279 delete component.rcs_k;
280 }
281 state.isDirty = true;
282 touched = true;
283 }
284 });
285 if (touched) {
286 const platform = get().platforms.find((p) => p.id === platformId);
287 const component = platform?.components.find(
288 (c) => c.id === componentId
289 );
290 if (component) {
291 void enqueueGranularSync(
292 'Target',
293 component.id,
294 JSON.stringify(
295 cleanObject(serializeComponentInner(component))
296 )
297 );
298 }
299 }
300 },
301 fetchPlatformPath: async (platformId) => {
302 const { platforms, showError, globalParameters } = get();
303 const platform = platforms.find((p) => p.id === platformId);
304
305 if (!platform) return;
306
307 // 1. Fetch/Calculate Motion Path
308 const { waypoints, interpolation } = platform.motionPath;
309 let newPathPoints: {
310 x: number;
311 y: number;
312 z: number;
313 vx: number;
314 vy: number;
315 vz: number;
316 }[] = [];
317
318 try {
319 if (waypoints.length < 2 || interpolation === 'static') {
320 // Static or single point: Calculate directly on frontend (velocity 0)
321 newPathPoints = waypoints.map((wp) => ({
322 x: wp.x,
323 y: wp.altitude,
324 z: -wp.y, // ENU Y -> Three JS -Z
325 vx: 0,
326 vy: 0,
327 vz: 0,
328 }));
329 } else {
330 // Dynamic: Fetch interpolated points from Backend
331 const points = await invoke<InterpolatedPoint[]>(
332 'get_interpolated_motion_path',
333 {
334 waypoints,
335 interpType: interpolation as InterpolationType,
336 numPoints: NUM_PATH_POINTS,
337 }
338 );
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) => ({
343 x: p.x,
344 y: p.z,
345 z: -p.y,
346 vx: p.vx,
347 vy: p.vz,
348 vz: -p.vy,
349 }));
350 }
351 } catch (error) {
352 const msg = error instanceof Error ? error.message : String(error);
353 console.error(
354 `Failed to fetch motion path for ${platform.name}:`,
355 msg
356 );
357 showError(`Failed to get motion path for ${platform.name}: ${msg}`);
358 // Fallback to empty to prevent stale data
359 newPathPoints = [];
360 }
361
362 // 2. Fetch/Calculate Rotation Path
363 const { rotation } = platform;
364 let newRotationPoints:
365 | { azimuth: number; elevation: number }[]
366 | undefined = undefined;
367
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.
371 if (
372 rotWaypoints.length >= 2 &&
373 rotation.interpolation !== 'static'
374 ) {
375 try {
376 const points = await invoke<InterpolatedRotationPoint[]>(
377 'get_interpolated_rotation_path',
378 {
379 waypoints: rotWaypoints,
380 interpType:
381 rotation.interpolation as InterpolationType,
382 angleUnit: globalParameters.rotationAngleUnit,
383 numPoints: NUM_PATH_POINTS,
384 }
385 );
386 newRotationPoints = points.map((p) => ({
387 azimuth: p.azimuth,
388 elevation: p.elevation,
389 }));
390 } catch (error) {
391 const msg =
392 error instanceof Error ? error.message : String(error);
393 console.error(
394 `Failed to fetch rotation path for ${platform.name}:`,
395 msg
396 );
397 // Log error but don't break the whole update; standard calc will fallback to first waypoint
398 }
399 }
400 }
401
402 // 3. Update Store
403 set((state) => {
404 const p = state.platforms.find((p) => p.id === platformId);
405 if (p) {
406 p.pathPoints = newPathPoints;
407 p.rotationPathPoints = newRotationPoints;
408 }
409 });
410 },
411});