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 { StateCreator } from 'zustand';
5import { v4 as uuidv4 } from 'uuid';
6import { invoke } from '@tauri-apps/api/core';
7import {
8 ScenarioStore,
9 PlatformActions,
10 Platform,
11 PlatformComponent,
12} from '../types';
13import { createDefaultPlatform } from '../defaults';
14
15const NUM_PATH_POINTS = 100;
16type InterpolationType = 'static' | 'linear' | 'cubic';
17interface InterpolatedPoint {
18 x: number;
19 y: number;
20 z: number;
21 vx: number;
22 vy: number;
23 vz: number;
24}
25
26interface InterpolatedRotationPoint {
27 azimuth_deg: number;
28 elevation_deg: number;
29}
30
31export const createPlatformSlice: StateCreator<
32 ScenarioStore,
33 [['zustand/immer', never]],
34 [],
35 PlatformActions
36> = (set, get) => ({
37 addPlatform: () =>
38 set((state) => {
39 const id = uuidv4();
40 const newName = `Platform ${state.platforms.length + 1}`;
41 const newPlatform: Platform = {
42 ...createDefaultPlatform(),
43 id,
44 name: newName,
45 };
46 // Defaults to empty components list
47 state.platforms.push(newPlatform);
48 state.isDirty = true;
49 }),
50 addPositionWaypoint: (platformId) =>
51 set((state) => {
52 const platform = state.platforms.find((p) => p.id === platformId);
53 if (platform) {
54 platform.motionPath.waypoints.push({
55 id: uuidv4(),
56 x: 0,
57 y: 0,
58 altitude: 0,
59 time: 0,
60 });
61 state.isDirty = true;
62 }
63 }),
64 removePositionWaypoint: (platformId, waypointId) =>
65 set((state) => {
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
70 );
71 if (index > -1) {
72 platform.motionPath.waypoints.splice(index, 1);
73 state.isDirty = true;
74 }
75 }
76 }),
77 addRotationWaypoint: (platformId) =>
78 set((state) => {
79 const platform = state.platforms.find((p) => p.id === platformId);
80 if (platform?.rotation.type === 'path') {
81 platform.rotation.waypoints.push({
82 id: uuidv4(),
83 azimuth: 0,
84 elevation: 0,
85 time: 0,
86 });
87 state.isDirty = true;
88 }
89 }),
90 removeRotationWaypoint: (platformId, waypointId) =>
91 set((state) => {
92 const platform = state.platforms.find((p) => p.id === platformId);
93 if (
94 platform?.rotation.type === 'path' &&
95 platform.rotation.waypoints.length > 1
96 ) {
97 const index = platform.rotation.waypoints.findIndex(
98 (wp) => wp.id === waypointId
99 );
100 if (index > -1) {
101 platform.rotation.waypoints.splice(index, 1);
102 state.isDirty = true;
103 }
104 }
105 }),
106 addPlatformComponent: (platformId, componentType) =>
107 set((state) => {
108 const platform = state.platforms.find((p) => p.id === platformId);
109 if (!platform) return;
110
111 const id = uuidv4();
112 const name = `${platform.name} ${
113 componentType.charAt(0).toUpperCase() + componentType.slice(1)
114 }`;
115 let newComponent: PlatformComponent;
116
117 switch (componentType) {
118 case 'monostatic':
119 newComponent = {
120 id,
121 type: 'monostatic',
122 name,
123 radarType: 'pulsed',
124 window_skip: 0,
125 window_length: 1e-5,
126 prf: 1000,
127 antennaId: null,
128 waveformId: null,
129 timingId: null,
130 noiseTemperature: 290,
131 noDirectPaths: false,
132 noPropagationLoss: false,
133 schedule: [],
134 };
135 break;
136 case 'transmitter':
137 newComponent = {
138 id,
139 type: 'transmitter',
140 name,
141 radarType: 'pulsed',
142 prf: 1000,
143 antennaId: null,
144 waveformId: null,
145 timingId: null,
146 schedule: [],
147 };
148 break;
149 case 'receiver':
150 newComponent = {
151 id,
152 type: 'receiver',
153 name,
154 radarType: 'pulsed',
155 window_skip: 0,
156 window_length: 1e-5,
157 prf: 1000,
158 antennaId: null,
159 timingId: null,
160 noiseTemperature: 290,
161 noDirectPaths: false,
162 noPropagationLoss: false,
163 schedule: [],
164 };
165 break;
166 case 'target':
167 newComponent = {
168 id,
169 type: 'target',
170 name,
171 rcs_type: 'isotropic',
172 rcs_value: 1,
173 rcs_model: 'constant',
174 };
175 break;
176 default:
177 return;
178 }
179 platform.components.push(newComponent);
180 state.isDirty = true;
181 }),
182 removePlatformComponent: (platformId, componentId) =>
183 set((state) => {
184 const platform = state.platforms.find((p) => p.id === platformId);
185 if (platform) {
186 const index = platform.components.findIndex(
187 (c) => c.id === componentId
188 );
189 if (index > -1) {
190 platform.components.splice(index, 1);
191 state.isDirty = true;
192 }
193 }
194 }),
195 setPlatformRcsModel: (platformId, componentId, newModel) =>
196 set((state) => {
197 const platform = state.platforms.find((p) => p.id === platformId);
198 const component = platform?.components.find(
199 (c) => c.id === componentId
200 );
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;
206 }
207 } else {
208 delete component.rcs_k;
209 }
210 state.isDirty = true;
211 }
212 }),
213 fetchPlatformPath: async (platformId) => {
214 const { platforms, showError } = get();
215 const platform = platforms.find((p) => p.id === platformId);
216
217 if (!platform) return;
218
219 // 1. Fetch/Calculate Motion Path
220 const { waypoints, interpolation } = platform.motionPath;
221 let newPathPoints: {
222 x: number;
223 y: number;
224 z: number;
225 vx: number;
226 vy: number;
227 vz: number;
228 }[] = [];
229
230 try {
231 if (waypoints.length < 2 || interpolation === 'static') {
232 // Static or single point: Calculate directly on frontend (velocity 0)
233 newPathPoints = waypoints.map((wp) => ({
234 x: wp.x,
235 y: wp.altitude,
236 z: -wp.y, // ENU Y -> Three JS -Z
237 vx: 0,
238 vy: 0,
239 vz: 0,
240 }));
241 } else {
242 // Dynamic: Fetch interpolated points from Backend
243 const points = await invoke<InterpolatedPoint[]>(
244 'get_interpolated_motion_path',
245 {
246 waypoints,
247 interpType: interpolation as InterpolationType,
248 numPoints: NUM_PATH_POINTS,
249 }
250 );
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) => ({
255 x: p.x,
256 y: p.z,
257 z: -p.y,
258 vx: p.vx,
259 vy: p.vz,
260 vz: -p.vy,
261 }));
262 }
263 } catch (error) {
264 const msg = error instanceof Error ? error.message : String(error);
265 console.error(
266 `Failed to fetch motion path for ${platform.name}:`,
267 msg
268 );
269 showError(`Failed to get motion path for ${platform.name}: ${msg}`);
270 // Fallback to empty to prevent stale data
271 newPathPoints = [];
272 }
273
274 // 2. Fetch/Calculate Rotation Path
275 const { rotation } = platform;
276 let newRotationPoints:
277 | { azimuth: number; elevation: number }[]
278 | undefined = undefined;
279
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.
283 if (
284 rotWaypoints.length >= 2 &&
285 rotation.interpolation !== 'static'
286 ) {
287 try {
288 const points = await invoke<InterpolatedRotationPoint[]>(
289 'get_interpolated_rotation_path',
290 {
291 waypoints: rotWaypoints,
292 interpType:
293 rotation.interpolation as InterpolationType,
294 numPoints: NUM_PATH_POINTS,
295 }
296 );
297 newRotationPoints = points.map((p) => ({
298 azimuth: p.azimuth_deg,
299 elevation: p.elevation_deg,
300 }));
301 } catch (error) {
302 const msg =
303 error instanceof Error ? error.message : String(error);
304 console.error(
305 `Failed to fetch rotation path for ${platform.name}:`,
306 msg
307 );
308 // Log error but don't break the whole update; standard calc will fallback to first waypoint
309 }
310 }
311 }
312
313 // 3. Update Store
314 set((state) => {
315 const p = state.platforms.find((p) => p.id === platformId);
316 if (p) {
317 p.pathPoints = newPathPoints;
318 p.rotationPathPoints = newRotationPoints;
319 }
320 });
321 },
322});