From 88ce33b46f65d1e592057e51128b995d65eb34eb Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Tue, 19 Nov 2024 02:04:42 -0500 Subject: [PATCH] ui: sim state monitoring, play/pause --- Cargo.toml | 3 +- docs/devlog.md | 18 +++ src/app3d/mod.rs | 2 - src/app3d/scene/camera.rs | 2 +- src/app3d/ui/core.rs | 56 +++++++++ src/app3d/ui/dev_tools.rs | 2 +- src/app3d/ui/mod.rs | 31 +---- src/app3d/ui/monitors.rs | 235 ++++++++++++++++++++++++++++-------- src/{app3d => }/controls.rs | 25 ++-- src/lib.rs | 17 +-- src/main.rs | 4 +- src/simulator/atmosphere.rs | 80 ++++++++---- src/simulator/forces/mod.rs | 9 +- src/simulator/mod.rs | 20 +++ src/simulator/properties.rs | 12 +- 15 files changed, 386 insertions(+), 130 deletions(-) create mode 100644 src/app3d/ui/core.rs rename src/{app3d => }/controls.rs (85%) diff --git a/Cargo.toml b/Cargo.toml index 95225e8..7434818 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ dev_native = [ "dev", # Enable system information plugin for native dev builds. "bevy/sysinfo_plugin", + "iyes_perf_ui/sysinfo", ] config-files = ["ron", "bevy_common_assets", "serde"] inspect = ["bevy-inspector-egui", "bevy_panorbit_camera/bevy_egui"] @@ -35,7 +36,7 @@ avian3d = { version = "0.1.2", features = ["debug-plugin"] } # ui dependencies bevy_panorbit_camera = { version = "0.20.0" } bevy-inspector-egui = { version = "0.27.0", features = ["highlight_changes"], optional = true } -iyes_perf_ui = { version = "0.3.0" } +iyes_perf_ui = "0.3.0" # file io dependencies bevy_common_assets = { version = "0.11.0", features = ["ron"], optional = true } ron = { version = "0.8.1", optional = true } diff --git a/docs/devlog.md b/docs/devlog.md index 3a074fc..a87081c 100644 --- a/docs/devlog.md +++ b/docs/devlog.md @@ -1,5 +1,23 @@ # development log +## 2024-11-18 again + +I think I was a bit naive to install `bevy-trait-query`. It works for now but in +the future we should really move away from it. It is currently a crutch. + +For some reason when the balloon bounces, it accelerates upward to oblivion. I +added some errors to stop the simulation when the balloon goes out of bounds, +but right now it panics. Not great but better than nothing. Best I can do is +bring in the "out of bounds" level to be less than the true bounds. + +The drag force is the one that is causing the bad acceleration. Weight and +buoyancy don't change with time since the balloon has constant size right now. + +- Added pause/play controls. Default key is `Space`. +- Added a new ui that displays the simulation state. +- Added a new ui that displays forces in real time, assuming only one balloon. +- Added an `Anomaly` state to the sim that is supposed to freeze. + ## 2024-11-18 Iterating with AI on the drag calculations. It is set up to perform a raycast diff --git a/src/app3d/mod.rs b/src/app3d/mod.rs index 9995a43..40c0245 100644 --- a/src/app3d/mod.rs +++ b/src/app3d/mod.rs @@ -1,8 +1,6 @@ mod scene; mod ui; -mod controls; // Re-export the plugins so they can be added to the app with `app.add_plugins`. pub use scene::ScenePlugin; pub use ui::InterfacePlugins; -pub use controls::{ControlsPlugin, KeyBindingsConfig}; diff --git a/src/app3d/scene/camera.rs b/src/app3d/scene/camera.rs index 114884c..8372f55 100644 --- a/src/app3d/scene/camera.rs +++ b/src/app3d/scene/camera.rs @@ -2,7 +2,7 @@ use bevy::prelude::*; use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin}; use avian3d::math::TAU; -use crate::app3d::KeyBindingsConfig; +use crate::controls::KeyBindingsConfig; pub struct CameraPlugin; diff --git a/src/app3d/ui/core.rs b/src/app3d/ui/core.rs new file mode 100644 index 0000000..b1b8d9b --- /dev/null +++ b/src/app3d/ui/core.rs @@ -0,0 +1,56 @@ +use bevy::{app::PluginGroupBuilder, prelude::*}; +use iyes_perf_ui::prelude::*; + +use crate::controls::KeyBindingsConfig; +use crate::simulator::SimState; + +use super::*; + +/// A plugin group that includes all interface-related plugins +pub struct InterfacePlugins; + +impl PluginGroup for InterfacePlugins { + fn build(self) -> PluginGroupBuilder { + PluginGroupBuilder::start::() + .add(CoreUiPlugin) + .add(PausePlayPlugin) + .add(monitors::MonitorsPlugin) + } +} + +/// Base UI plugin. This sets up the base plugins that all other ui plugins +/// need. .Placeholder for now +pub struct CoreUiPlugin; + +impl Plugin for CoreUiPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(( + PerfUiPlugin, + #[cfg(feature = "dev")] + dev_tools::plugin, + )); + } +} + +pub struct PausePlayPlugin; + +impl Plugin for PausePlayPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Update, toggle_pause); + } +} + +fn toggle_pause( + sim_state: Res>, + mut next_state: ResMut>, + key_input: Res>, + key_bindings: Res, +) { + if key_input.just_pressed(key_bindings.time_controls.toggle_pause) { + match sim_state.as_ref().get() { + SimState::Stopped => next_state.set(SimState::Running), + SimState::Running => next_state.set(SimState::Stopped), + _ => () + } + } +} diff --git a/src/app3d/ui/dev_tools.rs b/src/app3d/ui/dev_tools.rs index c8484af..dd4dae6 100644 --- a/src/app3d/ui/dev_tools.rs +++ b/src/app3d/ui/dev_tools.rs @@ -16,7 +16,7 @@ use bevy::pbr::wireframe::{WireframeConfig, WireframePlugin}; use avian3d::debug_render::PhysicsDebugPlugin; -use crate::app3d::KeyBindingsConfig; +use crate::controls::KeyBindingsConfig; pub(super) fn plugin(app: &mut App) { // Toggle the debug overlay for UI. diff --git a/src/app3d/ui/mod.rs b/src/app3d/ui/mod.rs index 66d3000..af959cc 100644 --- a/src/app3d/ui/mod.rs +++ b/src/app3d/ui/mod.rs @@ -1,32 +1,7 @@ -// mod monitors; +mod core; +mod monitors; #[cfg(feature = "dev")] mod dev_tools; -use bevy::{app::PluginGroupBuilder, prelude::*}; -use iyes_perf_ui::prelude::*; - -/// A plugin group that includes all interface-related plugins -pub struct InterfacePlugins; - -impl PluginGroup for InterfacePlugins { - fn build(self) -> PluginGroupBuilder { - PluginGroupBuilder::start::() - .add(CoreUiPlugin) - .add(monitors::ForceMonitorPlugin) - } -} - -/// Base UI plugin. This sets up the base plugins that all other ui plugins -/// need. .Placeholder for now -pub struct CoreUiPlugin; - -impl Plugin for CoreUiPlugin { - fn build(&self, app: &mut App) { - app.add_plugins(( - PerfUiPlugin, - #[cfg(feature = "dev")] - dev_tools::plugin, - )); - } -} +pub use core::InterfacePlugins; diff --git a/src/app3d/ui/monitors.rs b/src/app3d/ui/monitors.rs index 81b3382..880806e 100644 --- a/src/app3d/ui/monitors.rs +++ b/src/app3d/ui/monitors.rs @@ -1,25 +1,41 @@ //! UI for monitoring the simulation. +#![allow(unused_imports)] +use bevy::{ + ecs::system::{lifetimeless::SRes, SystemParam}, + prelude::*, +}; +use iyes_perf_ui::{entry::PerfUiEntry, prelude::*, utils::format_pretty_float}; -use bevy::prelude::*; -use iyes_perf_ui::prelude::*; +use crate::simulator::{ + forces::{Buoyancy, Drag, Force, Weight}, + SimState, SimulatedBody, +}; -pub struct ForceMonitorPlugin ; +pub struct MonitorsPlugin; -impl Plugin for ForceMonitorPlugin { +impl Plugin for MonitorsPlugin { fn build(&self, app: &mut App) { - app.add_perf_ui_simple_entry::(); - app.add_perf_ui_simple_entry::(); - app.add_perf_ui_simple_entry::(); - app.add_perf_ui_simple_entry::(); + app.add_perf_ui_simple_entry::(); + app.add_perf_ui_simple_entry::(); + app.add_systems(Startup, spawn_monitors); + app.add_systems(Update, update_force_monitor_values); + app.init_resource::(); } } -// TODO: make a generic force monitor that takes a force trait and displays the -// force vector so we don't have to make a new monitor for each force type and -// redo all that boilerplate code. +fn spawn_monitors(mut commands: Commands) { + commands.spawn(( + PerfUiRoot { + position: PerfUiPosition::BottomRight, + ..default() + }, + SimStateMonitor::default(), + ForceMonitor::default(), + )); +} #[derive(Component)] -struct WeightForceMonitor { +struct SimStateMonitor { /// The label text to display, to allow customization pub label: String, /// Should we display units? @@ -32,51 +48,107 @@ struct WeightForceMonitor { pub digits: u8, /// Precision for formatting the string pub precision: u8, - /// Required to ensure the entry appears in the correct place in the Perf UI pub sort_key: i32, } -#[derive(Component)] -struct BuoyancyForceMonitor { - /// The label text to display, to allow customization - pub label: String, - /// Should we display units? - pub display_units: bool, - /// Highlight the value if it goes above this threshold - pub threshold_highlight: Option, - /// Support color gradients! - pub color_gradient: ColorGradient, - /// Width for formatting the string - pub digits: u8, - /// Precision for formatting the string - pub precision: u8, +impl Default for SimStateMonitor { + fn default() -> Self { + SimStateMonitor { + label: String::new(), + display_units: false, + threshold_highlight: Some(10.0), + color_gradient: ColorGradient::new_preset_gyr(0.0, 10.0, 100.0).unwrap(), + digits: 7, + precision: 0, + sort_key: iyes_perf_ui::utils::next_sort_key(), + } + } +} - /// Required to ensure the entry appears in the correct place in the Perf UI - pub sort_key: i32, +impl PerfUiEntry for SimStateMonitor { + type Value = SimState; + type SystemParam = SRes>; + + fn label(&self) -> &str { + if self.label.is_empty() { + "Simulation State" + } else { + &self.label + } + } + + fn sort_key(&self) -> i32 { + self.sort_key + } + + fn format_value(&self, value: &Self::Value) -> String { + match value { + SimState::Running => String::from("Running"), + SimState::Stopped => String::from("Stopped"), + SimState::Anomaly => String::from("ANOMALY") + } + } + + fn update_value( + &self, + state: &mut ::Item<'_, '_>, + ) -> Option { + Some(*state.as_ref().get()) + } + + // (optional) We should add a width hint, so that the displayed + // strings in the UI can be correctly aligned. + // This value represents the largest length the formatted string + // is expected to have. + fn width_hint(&self) -> usize { + // there is a helper we can use, since we use `format_pretty_float` + let w = iyes_perf_ui::utils::width_hint_pretty_float(self.digits, self.precision); + if self.display_units { + w + 2 + } else { + w + } + } + + // (optional) Called every frame to determine if a custom color should be used for the value + fn value_color(&self, value: &Self::Value) -> Option { + match *value { + SimState::Running => self.color_gradient.get_color_for_value(0.0), + SimState::Stopped => self.color_gradient.get_color_for_value(10.0), + _ => self.color_gradient.get_color_for_value(100.0), + } + } + + // (optional) Called every frame to determine if the value should be highlighted + fn value_highlight(&self, value: &Self::Value) -> bool { + self.threshold_highlight + .map(|_| value == &SimState::Stopped) + .unwrap_or(false) + } } -#[derive(Component)] -struct DragForceMonitor { - /// The label text to display, to allow customization - pub label: String, - /// Should we display units? - pub display_units: bool, - /// Highlight the value if it goes above this threshold - pub threshold_highlight: Option, - /// Support color gradients! - pub color_gradient: ColorGradient, - /// Width for formatting the string - pub digits: u8, - /// Precision for formatting the string - pub precision: u8, +#[derive(Resource, Reflect, Default)] +struct ForceMonitorResource { + pub weight: Vec3, + pub buoyancy: Vec3, + pub drag: Vec3, +} - /// Required to ensure the entry appears in the correct place in the Perf UI - pub sort_key: i32, +fn update_force_monitor_values( + mut force_resource: ResMut, + forces: Query<(&Weight, &Buoyancy, &Drag), With>, +) { + for (weight, bouyancy, drag) in forces.iter() { + // assume there's only one simulated body for now + force_resource.weight = weight.force(); + force_resource.buoyancy = bouyancy.force(); + force_resource.drag = drag.force(); + } } #[derive(Component)] -struct NetForceMonitor { +struct ForceMonitor { /// The label text to display, to allow customization pub label: String, /// Should we display units? @@ -89,7 +161,76 @@ struct NetForceMonitor { pub digits: u8, /// Precision for formatting the string pub precision: u8, - /// Required to ensure the entry appears in the correct place in the Perf UI pub sort_key: i32, } + +impl Default for ForceMonitor { + fn default() -> Self { + ForceMonitor { + label: String::new(), + display_units: true, + threshold_highlight: Some(10.0), + color_gradient: ColorGradient::new_preset_gyr(0.0, 10.0, 100.0).unwrap(), + digits: 12, + precision: 2, + sort_key: iyes_perf_ui::utils::next_sort_key(), + } + } +} + +impl PerfUiEntry for ForceMonitor { + type Value = (f32, f32, f32); + type SystemParam = SRes; + + fn label(&self) -> &str { + if self.label.is_empty() { + "Forces" + } else { + &self.label + } + } + + fn sort_key(&self) -> i32 { + self.sort_key + } + + fn format_value(&self, value: &Self::Value) -> String { + // we can use a premade helper function for nice-looking formatting + let mut f_g = format_pretty_float(self.digits, self.precision, value.0 as f64); + let mut f_b = format_pretty_float(self.digits, self.precision, value.1 as f64); + let mut f_d = format_pretty_float(self.digits, self.precision, value.2 as f64); + // (and append units to it) + if self.display_units { + f_g.push_str(" N"); + f_b.push_str(" N"); + f_d.push_str(" N"); + } + format!("Fg {:} Fb {:} Fd {:}", f_g, f_b, f_d) + } + + fn update_value( + &self, + force_resource: &mut ::Item<'_, '_>, + ) -> Option { + Some(( + force_resource.weight.length(), + force_resource.buoyancy.length(), + force_resource.drag.length(), + )) + } + + // (optional) We should add a width hint, so that the displayed + // strings in the UI can be correctly aligned. + // This value represents the largest length the formatted string + // is expected to have. + fn width_hint(&self) -> usize { + // there is a helper we can use, since we use `format_pretty_float` + let w = iyes_perf_ui::utils::width_hint_pretty_float(self.digits, self.precision); + if self.display_units { + w + 2 + } else { + w + } + } +} diff --git a/src/app3d/controls.rs b/src/controls.rs similarity index 85% rename from src/app3d/controls.rs rename to src/controls.rs index f275207..68ea9ae 100644 --- a/src/app3d/controls.rs +++ b/src/controls.rs @@ -8,19 +8,11 @@ impl Plugin for ControlsPlugin { } } -#[derive(Resource)] +#[derive(Resource, Default)] pub struct KeyBindingsConfig { pub camera_controls: CameraControls, pub debug_controls: DebugControls, -} - -impl Default for KeyBindingsConfig { - fn default() -> Self { - Self { - camera_controls: CameraControls::default(), - debug_controls: DebugControls::default(), - } - } + pub time_controls: TimeControls, } #[derive(Reflect)] @@ -38,6 +30,11 @@ pub struct DebugControls { pub toggle_perf_ui: KeyCode, } +#[derive(Reflect)] +pub struct TimeControls { + pub toggle_pause: KeyCode, +} + // ============================ DEFAULT KEYBINDINGS ============================ /// Defaults follow Blender conventions @@ -61,3 +58,11 @@ impl Default for DebugControls { } } } + +impl Default for TimeControls { + fn default() -> Self { + Self { + toggle_pause: KeyCode::Space, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index b9a6fac..1000bcb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,15 @@ -mod simulator; mod app3d; +mod controls; +mod simulator; #[cfg(feature = "config-files")] mod assets; use bevy::{asset::AssetMetaCheck, prelude::*}; -pub struct AppCorePlugin; +pub struct YahsPlugin; -impl Plugin for AppCorePlugin { +impl Plugin for YahsPlugin { fn build(&self, app: &mut App) { // Add Bevy plugins. app.add_plugins( @@ -34,14 +35,16 @@ impl Plugin for AppCorePlugin { }), ); - // Add the simulator plugins (that don't deal with graphics). - app.add_plugins(simulator::SimulatorPlugins); + // Add the simulator plugins that don't deal with graphics. + app.add_plugins(( + simulator::SimulatorPlugins, + controls::ControlsPlugin, + )); // Add the 3D application plugins. app.add_plugins(( - app3d::InterfacePlugins, app3d::ScenePlugin, - app3d::ControlsPlugin, + app3d::InterfacePlugins, )); } } diff --git a/src/main.rs b/src/main.rs index 1fbaf8b..7d22685 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,8 @@ #![cfg_attr(not(feature = "dev"), windows_subsystem = "windows")] use bevy::prelude::*; -use yahs::AppCorePlugin; +use yahs::YahsPlugin; fn main() -> AppExit { - App::new().add_plugins(AppCorePlugin).run() + App::new().add_plugins(YahsPlugin).run() } diff --git a/src/simulator/atmosphere.rs b/src/simulator/atmosphere.rs index f8b9aea..bc65af3 100644 --- a/src/simulator/atmosphere.rs +++ b/src/simulator/atmosphere.rs @@ -9,13 +9,14 @@ use bevy::prelude::*; use super::{ ideal_gas::{ideal_gas_density, GasSpecies}, - Density, Pressure, Temperature, + Density, Position, Pressure, SimState, SimulatedBody, Temperature, }; pub struct AtmospherePlugin; impl Plugin for AtmospherePlugin { fn build(&self, app: &mut App) { app.insert_resource(Atmosphere); + app.add_systems(Update, fault_if_out_of_bounds); } } @@ -24,16 +25,27 @@ impl Plugin for AtmospherePlugin { pub struct Atmosphere; impl Atmosphere { + pub const MAX_ALTITUDE: f32 = 84999.0; // small margin to avoid panics + pub const MIN_ALTITUDE: f32 = -56.0; // small margin to avoid panics + + pub fn out_of_bounds(&self, position: Vec3) -> bool { + match position.y { + y if (y > Atmosphere::MAX_ALTITUDE) => true, + y if (y < Atmosphere::MIN_ALTITUDE) => true, + _ => false, + } + } + /// Temperature (K) of the atmosphere at a position. pub fn temperature(&self, position: Vec3) -> Temperature { - // TODO: Look up temperature based on latitude, longitude, and altitude. - coesa_temperature(position.y) + // TODO: Look up temperature based on latitude, longitude, not just altitude + coesa_temperature(position.y).unwrap() // we should handle this better } /// Pressure (Pa) of the atmosphere at a position. pub fn pressure(&self, position: Vec3) -> Pressure { - // TODO: Look up pressure based on latitude, longitude, and altitude. - coesa_pressure(position.y) + // TODO: Look up pressure based on latitude, longitude, not just altitude + coesa_pressure(position.y).unwrap() // we should handle this better } /// Density (kg/m³) of the atmosphere at a position. @@ -46,44 +58,66 @@ impl Atmosphere { } } +/// If any of the simulated bodies are out of bounds, set the app state to anomaly +/// TODO: we should use an event for this +fn fault_if_out_of_bounds( + atmosphere: Res, + bodies: Query<(Entity, &Position), With>, + mut next_state: ResMut>, +) { + for (_, position) in bodies.iter() { + if atmosphere.out_of_bounds(position.0) { + next_state.set(SimState::Anomaly) + }; + } +} + /// Temperature (K) of the atmosphere at a given altitude (m). /// Only valid for altitudes below 85,000 meters. /// Based on the US Standard Atmosphere, 1976. (aka COESA) -fn coesa_temperature(altitude: f32) -> Temperature { +fn coesa_temperature(altitude: f32) -> Result { if (-57.0..11000.0).contains(&altitude) { - Temperature::from_celsius(15.04 - 0.00649 * altitude) + Ok(Temperature::from_celsius(15.04 - 0.00649 * altitude)) } else if (11000.0..25000.0).contains(&altitude) { - Temperature::from_celsius(-56.46) + Ok(Temperature::from_celsius(-56.46)) } else if (25000.0..85000.0).contains(&altitude) { - Temperature::from_celsius(-131.21 + 0.00299 * altitude) + Ok(Temperature::from_celsius(-131.21 + 0.00299 * altitude)) } else { - error!( + Err(format!( "Altitude {:}m is outside of the accepted range! Must be 0-85000m", altitude - ); - Temperature::from_celsius(0.0) + )) } } /// Pressure (Pa) of the atmosphere at a given altitude (m). /// Only valid for altitudes below 85,000 meters. /// Based on the US Standard Atmosphere, 1976. (aka COESA) -fn coesa_pressure(altitude: f32) -> Pressure { +fn coesa_pressure(altitude: f32) -> Result { if (-57.0..11000.0).contains(&altitude) { - Pressure::from_kilopascal( - 101.29 * f32::powf(coesa_temperature(altitude).kelvin() / 288.08, 5.256), - ) + Ok(Pressure::from_kilopascal( + 101.29 + * f32::powf( + coesa_temperature(altitude).unwrap_or_default().kelvin() / 288.08, + 5.256, + ), + )) } else if (11000.0..25000.0).contains(&altitude) { - Pressure::from_kilopascal(22.65 * f32::exp(1.73 - 0.000157 * altitude)) + Ok(Pressure::from_kilopascal( + 22.65 * f32::exp(1.73 - 0.000157 * altitude), + )) } else if (25000.0..85000.0).contains(&altitude) { - Pressure::from_kilopascal( - 2.488 * f32::powf(coesa_temperature(altitude).kelvin() / 216.6, -11.388), - ) + Ok(Pressure::from_kilopascal( + 2.488 + * f32::powf( + coesa_temperature(altitude).unwrap_or_default().kelvin() / 216.6, + -11.388, + ), + )) } else { - error!( + Err(format!( "Altitude {:}m is outside of the accepted range! Must be 0-85000m", altitude - ); - Pressure(0.0) + )) } } diff --git a/src/simulator/forces/mod.rs b/src/simulator/forces/mod.rs index 63a2e38..a5bb436 100644 --- a/src/simulator/forces/mod.rs +++ b/src/simulator/forces/mod.rs @@ -6,8 +6,13 @@ use avian3d::prelude::*; use bevy::prelude::*; use bevy_trait_query; -use super::{Atmosphere, Density, Mass, Volume, SimulatedBody}; +// Re-expert common forces +#[allow(unused_imports)] +pub use body::{Weight, Buoyancy}; +#[allow(unused_imports)] +pub use aero::Drag; +use super::{Atmosphere, Density, Mass, Volume, SimulatedBody}; pub struct ForcesPlugin; impl Plugin for ForcesPlugin { @@ -92,13 +97,13 @@ fn update_total_external_force( ) { // Iterate over each entity that has force vector components. for (mut physics_force_component, acting_forces, rigid_body) in body_forces.iter_mut() { + // Forces only act on dynamic bodies. Don't bother with other kinds. if rigid_body.is_dynamic() { let mut net_force = Vec3::ZERO; // reset the net force to zero // Iterate over each force vector component and compute its value. for force in acting_forces.iter() { net_force += force.force(); - info!("{}: {:?}", force.name(), force.force()); } physics_force_component.set_force(net_force); } diff --git a/src/simulator/mod.rs b/src/simulator/mod.rs index 858fc03..c24cd24 100644 --- a/src/simulator/mod.rs +++ b/src/simulator/mod.rs @@ -39,5 +39,25 @@ impl Plugin for CorePhysicsPlugin { ideal_gas::IdealGasPlugin, forces::ForcesPlugin, )); + app.init_state::(); + app.add_systems(Update, pause_physics_time); } } + +#[derive(States, Debug, Default, Clone, Copy, Hash, PartialEq, Eq)] +pub enum SimState { + #[default] + Running, + Stopped, + Anomaly, +} + +fn pause_physics_time( + sim_state: Res>, + mut physics_time: ResMut>) { + match sim_state.as_ref().get() { + SimState::Running => physics_time.unpause(), + SimState::Stopped => physics_time.pause(), + SimState::Anomaly => physics_time.pause(), + } +} diff --git a/src/simulator/properties.rs b/src/simulator/properties.rs index 7fc57c2..038e26d 100644 --- a/src/simulator/properties.rs +++ b/src/simulator/properties.rs @@ -52,7 +52,7 @@ impl Plugin for CorePropertiesPlugin { } /// Temperature (K) -#[derive(Component, Debug, Clone, Copy, PartialEq, Reflect)] +#[derive(Component, Debug, Default, Clone, Copy, PartialEq, Reflect)] #[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] pub struct Temperature(pub Scalar); @@ -109,7 +109,7 @@ impl Div for Temperature { } /// Pressure (Pa) -#[derive(Component, Debug, Clone, Copy, PartialEq, Reflect)] +#[derive(Component, Debug, Default, Clone, Copy, PartialEq, Reflect)] #[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] pub struct Pressure(pub Scalar); @@ -166,7 +166,7 @@ impl Div for Pressure { } /// The volume of a body in cubic meters. -#[derive(Component, Debug, Clone, Copy, PartialEq, Reflect)] +#[derive(Component, Debug, Default, Clone, Copy, PartialEq, Reflect)] #[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] pub struct Volume(pub Scalar); @@ -216,7 +216,7 @@ impl Div for Volume { } /// Density (kg/m³) -#[derive(Component, Debug, Clone, Copy, PartialEq, Reflect)] +#[derive(Component, Debug, Default, Clone, Copy, PartialEq, Reflect)] #[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] pub struct Density(pub Scalar); @@ -276,7 +276,7 @@ fn sync_avian_density(mut densities: Query<(&mut ColliderDensity, &Volume, &Mass } /// Mass (kg) -#[derive(Component, Debug, Clone, Copy, PartialEq, Reflect)] +#[derive(Component, Debug, Default, Clone, Copy, PartialEq, Reflect)] #[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] pub struct Mass(pub Scalar); @@ -335,7 +335,7 @@ impl Div for Mass { } /// Molar mass (kg/mol) of a substance. -#[derive(Component, Debug, Clone, Copy, PartialEq, Reflect)] +#[derive(Component, Debug, Default, Clone, Copy, PartialEq, Reflect)] #[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] pub struct MolarMass(pub Scalar);