1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
4import { StateCreator } from 'zustand';
5import { invoke } from '@tauri-apps/api/core';
12import { omit } from '@/utils/typeUtils.ts';
14// Helper to strip null/undefined values from an object before sending to backend
15const cleanObject = <T>(obj: T): T => {
16 if (Array.isArray(obj)) {
17 return obj.map(cleanObject) as unknown as T;
19 if (obj !== null && typeof obj === 'object') {
20 const newObj = {} as Record<string, unknown>;
21 for (const [key, value] of Object.entries(obj)) {
22 if (value !== null && value !== undefined) {
23 newObj[key] = cleanObject(value);
31// --- Backend-specific type definitions ---
35 type: TargetComponent['rcs_type'];
40 type: Exclude<TargetComponent['rcs_model'], 'constant'>;
45export const createBackendSlice: StateCreator<
47 [['zustand/immer', never]],
51 syncBackend: async () => {
52 set({ isBackendSyncing: true });
53 const { globalParameters, waveforms, timings, antennas, platforms } =
56 // Helper functions to map frontend asset IDs back to names for the backend
57 const findAntennaName = (id: string | null) =>
58 antennas.find((a) => a.id === id)?.name;
59 const findWaveformName = (id: string | null) =>
60 waveforms.find((p) => p.id === id)?.name;
61 const findTimingName = (id: string | null) =>
62 timings.find((t) => t.id === id)?.name;
64 const backendPlatforms = platforms.map((p) => {
65 const { components, motionPath, rotation, ...rest } = p;
67 // Map the list of components to backend objects
68 const backendComponents = components.map((component) => {
71 'radarType' in component && component.radarType === 'pulsed'
75 ...(component.type !== 'transmitter' && {
76 window_skip: component.window_skip,
77 window_length: component.window_length,
81 : 'radarType' in component &&
82 component.radarType === 'cw'
86 switch (component.type) {
92 antenna: findAntennaName(component.antennaId),
93 waveform: findWaveformName(
96 timing: findTimingName(component.timingId),
97 noise_temp: component.noiseTemperature,
98 nodirect: component.noDirectPaths,
99 nopropagationloss: component.noPropagationLoss,
100 schedule: component.schedule,
107 name: component.name,
109 antenna: findAntennaName(component.antennaId),
110 waveform: findWaveformName(
113 timing: findTimingName(component.timingId),
114 schedule: component.schedule,
121 name: component.name,
123 antenna: findAntennaName(component.antennaId),
124 timing: findTimingName(component.timingId),
125 noise_temp: component.noiseTemperature,
126 nodirect: component.noDirectPaths,
127 nopropagationloss: component.noPropagationLoss,
128 schedule: component.schedule,
134 const targetObj: BackendTarget = {
135 name: component.name,
137 type: component.rcs_type,
138 value: component.rcs_value,
139 filename: component.rcs_filename,
142 if (component.rcs_model !== 'constant') {
144 type: component.rcs_model,
148 compObj = { target: targetObj };
152 return cleanObject(compObj);
155 const backendRotation: Record<string, unknown> = {};
156 if (rotation.type === 'fixed') {
157 const r = omit(rotation, 'type');
158 backendRotation.fixedrotation = {
159 interpolation: 'constant',
160 startazimuth: r.startAzimuth,
161 startelevation: r.startElevation,
162 azimuthrate: r.azimuthRate,
163 elevationrate: r.elevationRate,
166 const r = omit(rotation, 'type');
167 backendRotation.rotationpath = {
168 interpolation: r.interpolation,
169 rotationwaypoints: r.waypoints.map((wp) => omit(wp, 'id')),
176 interpolation: motionPath.interpolation,
177 positionwaypoints: motionPath.waypoints.map((wp) =>
182 components: backendComponents,
186 const backendWaveforms = waveforms.map((w) => {
187 const waveformContent =
188 w.waveformType === 'cw'
190 : { pulsed_from_file: { filename: w.filename } };
195 carrier_frequency: w.carrier_frequency,
200 const backendTimings = timings.map((t: Timing) => {
201 const rest = omit(t, 'id', 'type');
205 freq_offset: t.freqOffset,
206 random_freq_offset_stdev: t.randomFreqOffsetStdev,
207 phase_offset: t.phaseOffset,
208 random_phase_offset_stdev: t.randomPhaseOffsetStdev,
209 noise_entries: t.noiseEntries.map((entry) => omit(entry, 'id')),
211 // Remove noise_entries array if it's empty
212 if (timingObj.noise_entries?.length === 0) {
213 delete (timingObj as Partial<typeof timingObj>).noise_entries;
218 const backendAntennas = antennas.map((a) =>
219 omit(a, 'id', 'type', 'meshScale')
229 } = globalParameters;
235 randomseed: random_seed,
236 oversample: oversample_ratio,
237 coordinatesystem: coordinateSystem,
240 const scenarioJson = {
242 name: globalParameters.simulation_name,
243 parameters: cleanObject(gp_params),
244 waveforms: cleanObject(backendWaveforms),
245 timings: cleanObject(backendTimings),
246 antennas: cleanObject(backendAntennas),
247 platforms: backendPlatforms,
252 const jsonPayload = JSON.stringify(scenarioJson, null, 2);
253 await invoke('update_scenario_from_json', {
256 console.log('Successfully synced state to backend.');
259 state.isBackendSyncing = false;
260 state.backendVersion += 1;
263 console.error('Failed to sync state to backend:', error);
264 set({ isBackendSyncing: false });
268 fetchFromBackend: async () => {
270 const jsonState = await invoke<string>('get_scenario_as_json');
271 const scenarioData = JSON.parse(jsonState);
272 get().loadScenario(scenarioData);
274 console.error('Failed to fetch state from backend:', error);