1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
4//! # Tauri Application Library Entry Point
6//! This module provides the main entry point for the FERS Tauri desktop application.
7//! It bridges the Rust/Tauri frontend with the C++ `libfers` simulation engine via
8//! a Foreign Function Interface (FFI).
10//! ## Architecture Overview
12//! The application follows a three-layer architecture:
14//! 1. **Frontend (TypeScript/React)**: Provides the user interface for scenario
15//! editing and visualization.
16//! 2. **Middle Layer (Rust/Tauri)**: This module. Exposes Tauri commands that the
17//! frontend can invoke, and manages the simulation state via a thread-safe wrapper.
18//! 3. **Backend (C++ via FFI)**: The `libfers` library that performs the actual
19//! simulation computations, parsing, and serialization.
23//! The `FersContext` (which wraps the C++ simulation state) is protected by a `Mutex`
24//! and stored in Tauri's managed state. This ensures that concurrent calls from the
25//! UI are serialized, preventing data races on the non-thread-safe C++ object.
29//! All functions annotated with `#[tauri::command]` are exposed to the frontend via
30//! Tauri's IPC mechanism. They can be invoked asynchronously from JavaScript/TypeScript.
35use tauri::{AppHandle, Emitter, Manager, State};
37/// Data structure for a single motion waypoint received from the UI.
39/// Coordinates should be in the scenario's define frame (e.g. ENU).
40#[derive(serde::Serialize, serde::Deserialize)]
41pub struct MotionWaypoint {
44 /// Easting/X coordinate in meters.
46 /// Northing/Y coordinate in meters.
48 /// Altitude/Z coordinate in meters (MSL).
52/// Enum for the interpolation type received from the UI.
53#[derive(serde::Serialize, serde::Deserialize)]
54#[serde(rename_all = "lowercase")]
55pub enum InterpolationType {
61/// Data structure for a single interpolated point sent back to the UI.
63/// Represents the physical state of a platform at a specific time step.
64#[derive(serde::Serialize, serde::Deserialize)]
65pub struct InterpolatedMotionPoint {
66 /// X position in meters.
68 /// Y position in meters.
70 /// Z position in meters.
72 /// X velocity in m/s.
74 /// Y velocity in m/s.
76 /// Z velocity in m/s.
80/// Data structure for a single rotation waypoint received from the UI.
81#[derive(serde::Serialize, serde::Deserialize)]
82pub struct RotationWaypoint {
85 /// Azimuth in compass degrees (0=North, 90=East).
87 /// Elevation in degrees (positive up).
91/// Data structure for a single interpolated rotation point sent back to the UI.
92#[derive(serde::Serialize, serde::Deserialize)]
93pub struct InterpolatedRotationPoint {
94 /// Azimuth in compass degrees.
96 /// Elevation in degrees.
100/// Type alias for the managed Tauri state that holds the simulation context.
102/// The `FersContext` is wrapped in a `Mutex` to ensure thread-safe access, as Tauri
103/// may invoke commands from multiple threads concurrently. This alias simplifies
104/// the function signatures of Tauri commands.
105type FersState = Mutex<fers_api::FersContext>;
107// --- Tauri Commands ---
109/// Loads a FERS scenario from an XML file into the simulation context.
111/// This command replaces any existing in-memory scenario with the one parsed from
112/// the specified file. The file path is provided by the user via the frontend dialog.
116/// * `filepath` - The absolute or relative path to the FERS XML scenario file.
117/// * `state` - Tauri-managed state containing the shared `FersContext`.
121/// * `Ok(())` if the scenario was successfully loaded.
122/// * `Err(String)` containing an error message if loading failed (e.g., file not found,
123/// invalid XML, or a Mutex lock error).
125/// # Example (from frontend)
128/// import { invoke } from '@tauri-apps/api/core';
129/// await invoke('load_scenario_from_xml_file', { filepath: '/path/to/scenario.xml' });
132fn load_scenario_from_xml_file(
134 state: State<'_, FersState>,
135) -> Result<(), String> {
136 state.lock().map_err(|e| e.to_string())?.load_scenario_from_xml_file(&filepath)
139/// Retrieves the current in-memory scenario as a JSON string.
141/// This command serializes the simulation state into JSON format, allowing the
142/// frontend to display and edit the scenario. The JSON structure mirrors the
143/// internal representation used by `libfers`.
147/// * `state` - Tauri-managed state containing the shared `FersContext`.
151/// * `Ok(String)` containing the JSON representation of the scenario.
152/// * `Err(String)` containing an error message if serialization failed or if the
153/// Mutex could not be locked.
155/// # Example (from frontend)
158/// import { invoke } from '@tauri-apps/api/core';
159/// const scenarioJson = await invoke<string>('get_scenario_as_json');
160/// const scenario = JSON.parse(scenarioJson);
163fn get_scenario_as_json(state: State<'_, FersState>) -> Result<String, String> {
164 state.lock().map_err(|e| e.to_string())?.get_scenario_as_json()
167/// Retrieves the current in-memory scenario as a FERS XML string.
169/// This command is typically used when the user wants to export the scenario
170/// (potentially modified in the UI) back to the standard FERS XML format for
171/// sharing or archival.
175/// * `state` - Tauri-managed state containing the shared `FersContext`.
179/// * `Ok(String)` containing the XML representation of the scenario.
180/// * `Err(String)` containing an error message if serialization failed or if the
181/// Mutex could not be locked.
183/// # Example (from frontend)
186/// import { invoke } from '@tauri-apps/api/core';
187/// const scenarioXml = await invoke<string>('get_scenario_as_xml');
188/// // Save scenarioXml to a file using Tauri's fs plugin
191fn get_scenario_as_xml(state: State<'_, FersState>) -> Result<String, String> {
192 state.lock().map_err(|e| e.to_string())?.get_scenario_as_xml()
195/// Updates the in-memory scenario from a JSON string provided by the frontend.
197/// This is the primary method for applying changes made in the UI back to the
198/// simulation engine. The JSON is deserialized and used to rebuild the internal
199/// C++ world representation.
203/// * `json` - A JSON string representing the modified scenario structure.
204/// * `state` - Tauri-managed state containing the shared `FersContext`.
208/// * `Ok(())` if the scenario was successfully updated.
209/// * `Err(String)` containing an error message if deserialization failed, the JSON
210/// structure was invalid, or the Mutex could not be locked.
212/// # Example (from frontend)
215/// import { invoke } from '@tauri-apps/api/core';
216/// const updatedScenario = { /* modified scenario object */ };
217/// await invoke('update_scenario_from_json', { json: JSON.stringify(updatedScenario) });
220fn update_scenario_from_json(json: String, state: State<'_, FersState>) -> Result<(), String> {
221 state.lock().map_err(|e| e.to_string())?.update_scenario_from_json(&json)
224/// Triggers the simulation based on the current in-memory scenario.
226/// This command immediately returns `Ok(())` and spawns a background thread to
227/// perform the actual computationally intensive simulation. This prevents the UI
228/// from freezing. The result of the simulation (success or failure) is
229/// communicated back to the frontend via Tauri events.
233/// * `app_handle` - The Tauri application handle, used to access managed state
238/// * `simulation-complete` - Emitted with `()` as payload on successful completion.
239/// * `simulation-error` - Emitted with a `String` error message on failure.
240/// * `simulation-progress` - Emitted periodically with `{ message: String, current: i32, total: i32 }`.
242fn run_simulation(app_handle: AppHandle) -> Result<(), String> {
243 // Clone the AppHandle so we can move it into the background thread.
244 let app_handle_clone = app_handle.clone();
246 // Spawn a new thread to run the blocking C++ simulation.
247 std::thread::spawn(move || {
248 // Retrieve the managed state within the new thread.
249 let fers_state: State<'_, FersState> = app_handle_clone.state();
250 let result = fers_state
252 .map_err(|e| e.to_string())
253 .and_then(|context| context.run_simulation(&app_handle_clone));
255 // Emit an event to the frontend based on the simulation result.
259 .emit("simulation-complete", ())
260 .expect("Failed to emit simulation-complete event");
264 .emit("simulation-error", e)
265 .expect("Failed to emit simulation-error event");
270 // Return immediately, allowing the UI to remain responsive.
274/// Generates a KML visualization file for the current in-memory scenario.
276/// This command spawns a background thread to handle file I/O and KML generation,
277/// preventing the UI from freezing. The result is communicated via events.
281/// * `output_path` - The absolute file path where the KML file should be saved.
282/// * `app_handle` - The Tauri application handle.
286/// * `kml-generation-complete` - Emitted with the output path `String` on success.
287/// * `kml-generation-error` - Emitted with a `String` error message on failure.
289fn generate_kml(output_path: String, app_handle: AppHandle) -> Result<(), String> {
290 let app_handle_clone = app_handle.clone();
291 std::thread::spawn(move || {
292 let fers_state: State<'_, FersState> = app_handle_clone.state();
293 let result = fers_state
295 .map_err(|e| e.to_string())
296 .and_then(|context| context.generate_kml(&output_path));
301 .emit("kml-generation-complete", &output_path)
302 .expect("Failed to emit kml-generation-complete event");
306 .emit("kml-generation-error", e)
307 .expect("Failed to emit kml-generation-error event");
314/// A stateless command to calculate an interpolated motion path.
316/// This command delegates to the `libfers` core to calculate a path from a given
317/// set of waypoints, ensuring the UI visualization is identical to the path
318/// the simulation would use.
321/// * `waypoints` - A vector of motion waypoints.
322/// * `interp_type` - The interpolation algorithm to use ('static', 'linear', 'cubic').
323/// * `num_points` - The desired number of points for the final path.
326/// * `Ok(Vec<InterpolatedPoint>)` - The calculated path points.
327/// * `Err(String)` - An error message if the path calculation failed.
329fn get_interpolated_motion_path(
330 waypoints: Vec<MotionWaypoint>,
331 interp_type: InterpolationType,
333) -> Result<Vec<InterpolatedMotionPoint>, String> {
334 fers_api::get_interpolated_motion_path(waypoints, interp_type, num_points)
337/// A stateless command to calculate an interpolated rotation path.
339/// This command delegates to the `libfers` core to calculate a rotation path from a given
340/// set of waypoints. It is used by the UI to preview how the simulation will interpolate
341/// orientation changes over time.
344/// * `waypoints` - A vector of rotation waypoints (azimuth/elevation in compass degrees).
345/// * `interp_type` - The interpolation algorithm to use ('static', 'linear', 'cubic').
346/// * `num_points` - The desired number of points for the final path.
349/// * `Ok(Vec<InterpolatedRotationPoint>)` - The calculated rotation points.
350/// * `Err(String)` - An error message if the calculation failed.
352fn get_interpolated_rotation_path(
353 waypoints: Vec<RotationWaypoint>,
354 interp_type: InterpolationType,
356) -> Result<Vec<InterpolatedRotationPoint>, String> {
357 fers_api::get_interpolated_rotation_path(waypoints, interp_type, num_points)
360/// Retrieves a 2D antenna gain pattern for visualization.
362/// This command samples the antenna model loaded in the current simulation context.
363/// It is stateful because it relies on the antenna assets defined in the loaded scenario.
366/// * `antenna_name` - The unique name of the antenna asset to sample.
367/// * `az_samples` - Resolution along the azimuth axis (e.g., 360).
368/// * `el_samples` - Resolution along the elevation axis (e.g., 180).
369/// * `frequency` - The frequency in Hz at which to calculate gain (relevant for frequency-dependent antennas).
370/// * `state` - The shared simulation state.
373/// * `Ok(AntennaPatternData)` - Struct containing flattened gain array and dimensions.
374/// * `Err(String)` - Error if antenna not found or context locked.
376fn get_antenna_pattern(
377 antenna_name: String,
381 state: State<'_, FersState>,
382) -> Result<fers_api::AntennaPatternData, String> {
383 state.lock().map_err(|e| e.to_string())?.get_antenna_pattern(
391/// Calculates visual radio links between platforms at a specific time.
393/// This command performs a lightweight geometric and physics check to determine
394/// which platforms can "see" each other. It distinguishes between monostatic,
395/// bistatic, and interference paths based on signal-to-noise ratios.
398/// * `time` - The simulation time in seconds to evaluate.
399/// * `state` - The shared simulation state containing platforms and physics models.
402/// * `Ok(Vec<VisualLink>)` - A list of renderable link segments with metadata (type, quality, label).
403/// * `Err(String)` - Error if context access fails.
407 state: State<'_, FersState>,
408) -> Result<Vec<fers_api::VisualLink>, String> {
409 state.lock().map_err(|e| e.to_string())?.calculate_preview_links(time)
412/// Initializes and runs the Tauri application.
414/// This function is the main entry point for the desktop application. It performs
415/// the following setup steps:
417/// 1. Creates a new `FersContext` by calling the FFI layer. If this fails, it
418/// indicates a linking or initialization problem with `libfers`.
419/// 2. Registers Tauri plugins for file dialogs, file system access, and shell operations.
420/// 3. Stores the `FersContext` in Tauri's managed state, protected by a `Mutex`.
421/// 4. Registers all Tauri commands so they can be invoked from the frontend.
422/// 5. Launches the Tauri application event loop.
426/// This function will panic if:
427/// * The `FersContext` cannot be created (indicating a problem with `libfers`).
428/// * The Tauri application fails to start due to misconfiguration.
432/// This function is typically called from `main.rs`:
435/// fers_ui_lib::run();
437#[cfg_attr(mobile, tauri::mobile_entry_point)]
439 // Attempt to create the FFI context. This validates that libfers is correctly linked.
440 let context = fers_api::FersContext::new()
441 .expect("Failed to create FERS context. Is libfers linked correctly?");
443 tauri::Builder::default()
444 // Register Tauri plugins for UI functionality
445 .plugin(tauri_plugin_dialog::init())
446 .plugin(tauri_plugin_opener::init())
447 .plugin(tauri_plugin_fs::init())
448 // Store the FersContext as managed state, accessible from all commands
449 .manage(Mutex::new(context))
450 // Register all Tauri commands that can be invoked from the frontend
451 .invoke_handler(tauri::generate_handler![
452 load_scenario_from_xml_file,
453 get_scenario_as_json,
455 update_scenario_from_json,
458 get_interpolated_motion_path,
459 get_interpolated_rotation_path,
463 .run(tauri::generate_context!())
464 .expect("error while running tauri application");
471 /// Verifies that the `libfers` C++ library is correctly linked.
473 /// This test ensures that:
474 /// 1. The Rust linker can find the `libfers.a` static library.
475 /// 2. All required FFI symbols are present and callable.
476 /// 3. The `FersContext::new()` wrapper successfully creates a C++ context.
478 /// If this test fails:
479 /// * Check that the `build.rs` script correctly specifies library search paths.
480 /// * Verify that the C++ libraries have been built by CMake.
481 /// * Ensure that `bindgen` generated the correct bindings from `api.h`.
485 /// This test will panic if `FersContext::new()` returns `None`, indicating
486 /// a failure to allocate or initialize the C++ context.
488 fn it_links_libfers_and_creates_context() {
489 // This test will fail at link time if the library is not found.
490 // It will fail at runtime if the symbols don't match or creation fails.
491 let _context = fers_api::FersContext::new().expect("FersContext::new() returned None");
493 // The context is automatically destroyed when it goes out of scope due to `Drop`.
494 // No explicit destroy call is needed.