From c5de4a3f71e7456f58c97ec471f0293de8461c55 Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Sat, 24 Feb 2024 13:22:03 -0500 Subject: [PATCH 01/47] select config from gui --- src/cli.rs | 8 ++++++-- src/gui/shell.rs | 13 ++++++------- src/gui/views.rs | 41 ++++++++++++++++++++++++++++----------- src/simulator/balloon.rs | 4 ++-- src/simulator/config.rs | 38 ++++++++++++++++++++++-------------- src/simulator/gas.rs | 4 ++-- src/simulator/schedule.rs | 6 +++--- 7 files changed, 72 insertions(+), 42 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 8b28a77..8b72495 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,7 +3,10 @@ use std::path::PathBuf; use clap::{Parser, Subcommand}; use log::error; -use crate::simulator::schedule::{AsyncSim, Rate}; +use crate::simulator::{ + config, + schedule::{AsyncSim, Rate} +}; #[derive(Parser)] #[clap(author, version, about)] @@ -74,7 +77,8 @@ pub fn parse_inputs() { pub fn start_sim(config: &PathBuf, outpath: &PathBuf) { // initialize the simulation - let mut sim = AsyncSim::new(config, outpath.clone()); + let parsed_config = config::parse_from_file(config); + let mut sim = AsyncSim::new(parsed_config, outpath.clone()); let mut rate_sleeper = Rate::new(1.0); // start the sim diff --git a/src/gui/shell.rs b/src/gui/shell.rs index 8e5dd88..f10ea15 100644 --- a/src/gui/shell.rs +++ b/src/gui/shell.rs @@ -9,7 +9,7 @@ use egui::{Context, Modifiers, ScrollArea, Ui}; use super::UiPanel; use crate::simulator::{ io::SimOutput, - config::{self, Config}, + config::Config, schedule::AsyncSim, }; @@ -18,18 +18,16 @@ use crate::simulator::{ #[cfg_attr(feature = "serde", serde(default))] pub struct Shell { screens: Screens, - config: Config, + config: Option, output: Option, run_handle: Option>, } impl Default for Shell { fn default() -> Self { - let default_config_path = PathBuf::from("config/default.toml"); - let config = config::parse_from_file(&default_config_path); Self { screens: Screens::default(), - config, + config: None, output: None, run_handle: None, } @@ -103,10 +101,11 @@ impl Shell { if self.run_handle.is_some() { panic!("Can't start again, sim already ran. Need to stop.") } - let config = self.config.clone(); let outpath = PathBuf::from("out.csv"); let init_state = Arc::new(Mutex::new(SimOutput::default())); - AsyncSim::run_sim(config, init_state, outpath) + if let Some(config) = self.config.clone() { + AsyncSim::run_sim(config, init_state, outpath) + } } } } diff --git a/src/gui/views.rs b/src/gui/views.rs index 09ac3ea..b537d7d 100644 --- a/src/gui/views.rs +++ b/src/gui/views.rs @@ -2,12 +2,14 @@ use egui::*; use egui_plot::{ Bar, BarChart, BoxElem, BoxPlot, BoxSpread, Legend, Line, Plot, }; - +use log::error; use crate::gui::View; use crate::simulator::config::{self, Config}; // ---------------------------------------------------------------------------- +const DEFAULT_CONFIG_PATH: &str = "config/default.toml"; + #[derive(PartialEq)] pub struct ConfigView { config: Config, @@ -18,13 +20,13 @@ impl Default for ConfigView { fn default() -> Self { Self { config: config::parse_from_file("config/default.toml"), - picked_path: None, + picked_path: Some(String::from(DEFAULT_CONFIG_PATH)), } } } impl super::UiPanel for ConfigView { fn name(&self) -> &'static str { - "🗠 Config" + "☰ Config" } fn show(&mut self, ctx: &Context, open: &mut bool) { @@ -39,22 +41,39 @@ impl super::UiPanel for ConfigView { impl super::View for ConfigView { fn ui(&mut self, ui: &mut Ui) { ui.horizontal(|ui| { - - if ui.button("Import").clicked() { + if ui.button("Load").clicked() { + self.load_config(); + } + if ui.button("Choose File").clicked() { if let Some(path) = rfd::FileDialog::new().pick_file() { self.picked_path = Some(path.display().to_string()); } } - if let Some(picked_path) = &self.picked_path { - self.config = config::parse_from_file(picked_path); - ui.horizontal(|ui| { - ui.monospace(picked_path); - }); - } + self.display_path(ui); }); } } +impl ConfigView { + fn path_string(&mut self) -> String { + self.picked_path.clone().unwrap_or(String::from("(none)")) + } + + fn load_config(&mut self) { + if let Some(picked_path) = &self.picked_path { + self.config = config::parse_from_file(picked_path); + } else { + error!("Choose a valid path first. Got: {:?}", self.path_string()); + } + } + + fn display_path(&mut self, ui: &mut Ui) { + ui.horizontal(|ui| { + ui.label("File:"); + ui.monospace(self.path_string()); + }); + } +} // ---------------------------------------------------------------------------- #[derive(PartialEq, Default)] diff --git a/src/simulator/balloon.rs b/src/simulator/balloon.rs index b5c211d..9ce2f40 100644 --- a/src/simulator/balloon.rs +++ b/src/simulator/balloon.rs @@ -258,11 +258,11 @@ impl Material { } } -#[derive(Copy, Clone, PartialEq, Deserialize)] +#[derive(Copy, Clone, Default, PartialEq, Deserialize)] pub enum MaterialType { // Species of gas with a known molar mass (kg/mol) Nothing, - Rubber, + #[default] Rubber, LDPE, LowDensityPolyethylene, } diff --git a/src/simulator/config.rs b/src/simulator/config.rs index 573b118..55d8e84 100644 --- a/src/simulator/config.rs +++ b/src/simulator/config.rs @@ -1,4 +1,5 @@ use serde::Deserialize; +use log::{info, error}; use super::balloon::MaterialType; use super::gas::GasSpecies; @@ -6,34 +7,41 @@ use std::{fs, path::Path}; pub fn parse_from_file>(path: P) -> Config { // Read the contents of the configuration file as string - let contents = match fs::read_to_string(path) { + match fs::read_to_string(path) { // If successful return the files text as `contents`. // `c` is a local variable. - Ok(c) => c, + Ok(contents) => parse(&contents), // Handle the `error` case. Err(_) => { // Write `msg` to `stderr`. - eprintln!("Could not read file."); - // Exit the program with exit code `1`. - std::process::exit(1); + error!("Could not read file!"); + Config::default() } - }; - parse(&contents) + } } pub fn parse(contents: &String) -> Config { // unpack the config TOML from string - toml::from_str(contents).unwrap() + match toml::from_str(contents) { + Ok(parsed) => { + info!("Parsed config:\n{:}", contents); + parsed + }, + Err(_) => { + error!("Could not parse config! Using defaults."); + Config::default() + } + } } -#[derive(Clone, Deserialize, PartialEq)] +#[derive(Clone, Default, Deserialize, PartialEq)] pub struct Config { pub environment: EnvConfig, pub balloon: BalloonConfig, pub bus: BusConfig, } -#[derive(Clone, Deserialize, PartialEq)] +#[derive(Clone, Default, Deserialize, PartialEq)] pub struct EnvConfig { pub real_time: bool, pub tick_rate_hz: f32, @@ -42,7 +50,7 @@ pub struct EnvConfig { pub initial_velocity_m_s: f32, } -#[derive(Clone, Deserialize, PartialEq)] +#[derive(Clone, Default, Deserialize, PartialEq)] pub struct BalloonConfig { pub material: MaterialType, // balloon material pub thickness_m: f32, // thickness of balloon membrane @@ -50,27 +58,27 @@ pub struct BalloonConfig { pub lift_gas: GasConfig, } -#[derive(Clone, Deserialize, PartialEq)] +#[derive(Clone, Default, Deserialize, PartialEq)] pub struct GasConfig { pub species: GasSpecies, pub mass_kg: f32, } -#[derive(Clone, Deserialize, PartialEq)] +#[derive(Clone, Default, Deserialize, PartialEq)] pub struct BusConfig { pub body: BodyConfig, pub parachute: ParachuteConfig, // pub control: ControlConfig, } -#[derive(Copy, Clone, Deserialize, PartialEq)] +#[derive(Copy, Clone, Default, Deserialize, PartialEq)] pub struct BodyConfig { pub mass_kg: f32, // mass of all components less ballast material pub drag_area_m2: f32, // effective area used for drag calculations during freefall pub drag_coeff: f32, // drag coefficient of the payload during freefall } -#[derive(Copy, Clone, Deserialize, PartialEq)] +#[derive(Copy, Clone, Default, Deserialize, PartialEq)] pub struct ParachuteConfig { pub total_mass_kg: f32, // mass of the parachute system (main + drogue) pub drogue_area_m2: f32, // drogue parachute effective area used for drag calculations diff --git a/src/simulator/gas.rs b/src/simulator/gas.rs index af8c7c9..4816ad9 100644 --- a/src/simulator/gas.rs +++ b/src/simulator/gas.rs @@ -23,12 +23,12 @@ use log::error; use serde::Deserialize; use std::fmt; -#[derive(Copy, Clone, Deserialize, PartialEq)] +#[derive(Copy, Clone, Default, Deserialize, PartialEq)] pub enum GasSpecies { // Species of gas with a known molar mass (kg/mol) Air, He, - Helium, + #[default] Helium, H2, Hydrogen, N2, diff --git a/src/simulator/schedule.rs b/src/simulator/schedule.rs index f1e3803..7589aba 100644 --- a/src/simulator/schedule.rs +++ b/src/simulator/schedule.rs @@ -13,7 +13,7 @@ use std::{ use crate::simulator::{ balloon::{Balloon, Material}, bus::{Body, ParachuteSystem}, - config::{self, Config}, + config::Config, forces, gas::{Atmosphere, GasVolume}, io::{SimCommands, SimOutput}, @@ -152,9 +152,9 @@ pub struct AsyncSim { } impl AsyncSim { - pub fn new(config_path: &PathBuf, outpath: PathBuf) -> Self { + pub fn new(config: Config, outpath: PathBuf) -> Self { Self { - config: config::parse_from_file(config_path), + config, sim_output: Arc::new(Mutex::new(SimOutput::default())), outpath, command_sender: None, From 855260195a1fb4190faa9e8f7a13198923f2e2c6 Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Mon, 28 Oct 2024 11:12:40 -0400 Subject: [PATCH 02/47] checkpoint: feeling out bevy implementations --- Cargo.toml | 40 +++-------- src/cli.rs | 101 -------------------------- src/config.rs | 89 +++++++++++++++++++++++ src/gui/mod.rs | 29 +++++++- src/gui/shell.rs | 152 +++++++++++++++++++++++++++++---------- src/gui/views.rs | 2 +- src/main.rs | 25 +++++-- src/simulator/balloon.rs | 3 - src/simulator/config.rs | 90 ----------------------- src/simulator/mod.rs | 12 ++-- 10 files changed, 265 insertions(+), 278 deletions(-) delete mode 100644 src/cli.rs create mode 100644 src/config.rs delete mode 100644 src/simulator/config.rs diff --git a/Cargo.toml b/Cargo.toml index b58b982..85dcf73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,48 +2,24 @@ name = "yahs" description = "Yet Another HAB Simulator" authors = ["Philip Linden "] -version = "0.2.0" +version = "0.3.0" edition = "2021" readme = "README.md" license-file = "LICENSE" [features] -default = ["gui"] -gui = [ - "egui", - "egui_extras", - "egui_plot", - "eframe", - "emath", - "rfd", -] +default = [] [dependencies] pretty_env_logger = "0.5.0" -libm = "0.2.1" -toml = "0.8.10" -clap = { version = "4.1", default-features = false, features = [ - "derive", - "std", - "help", - "usage", - "error-context", - "suggestions", -] } -csv = "1.2.1" serde = { version = "1.0.196", features = ["derive"] } log = { version = "0.4.20", features = ["release_max_level_debug"] } -egui = { version = "0.26.2", features = ["log", "serde"], optional = true } -egui_plot = { version = "0.26.2", features = ["serde"], optional = true } -eframe = { version = "0.26.2", features = ["persistence"], optional = true } -emath = { version = "0.26.2", optional = true } -egui_extras = { version = "0.26.2", features = [ - "chrono", - "datepicker", - "file", -], optional = true } -rfd = { version = "0.14.0", optional = true } -ultraviolet = { version = "0.9.2", features = ["serde"] } +egui = { version = "0.29.1", features = ["log", "serde"] } +egui_plot = { version = "0.29.0", features = ["serde"] } +egui_extras = { version = "0.29.1", features = ["file"] } +bevy = "0.14.2" +bevy_egui = "0.30.0" +avian3d = "0.1.2" [[bin]] name = "yahs" diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index 8b72495..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::path::PathBuf; - -use clap::{Parser, Subcommand}; -use log::error; - -use crate::simulator::{ - config, - schedule::{AsyncSim, Rate} -}; - -#[derive(Parser)] -#[clap(author, version, about)] -struct Cli { - #[clap(subcommand)] - command: Commands, -} - -#[derive(Subcommand)] -enum Commands { - /// Start a new simulation process - /// - /// Configure an asynchronous physics simulation in the background. This - /// simulation runs on the MFC with flight software code running in the - /// loop and logs the simulation output to a CSV file. - Start { - /// Sets a custom simulation config file - #[clap( - short, - long, - value_name = "TOML", - default_value = "config/default.toml" - )] - config: PathBuf, - - /// Sets a custom output file - #[clap(short, long, value_name = "CSV", default_value = "./out.csv")] - outpath: PathBuf, - }, - - /// Inspect a physics parameter in an existing simulation - Get { - /// Parameter to be inspect - param: String, - }, - - /// Modify a physics parameter in an existing simulation - Set { - /// Parameter to be modified - param: String, - /// New value to set - value: String, - }, - - /// Open a graphical user interface instead of the terminal interface - Gui, -} - -pub fn parse_inputs() { - // parse CLI input args and options - let cli = Cli::parse(); - match &cli.command { - Commands::Start { config, outpath } => { - start_sim(config, outpath); - } - Commands::Gui => { - #[cfg(feature = "gui")] - start_gui(); - - #[cfg(not(feature = "gui"))] - error!("GUI feature not enabled. Reinstall with `--features gui`") - } - _ => { - error!("Command not implemented yet!") - } - } -} - -pub fn start_sim(config: &PathBuf, outpath: &PathBuf) { - // initialize the simulation - let parsed_config = config::parse_from_file(config); - let mut sim = AsyncSim::new(parsed_config, outpath.clone()); - let mut rate_sleeper = Rate::new(1.0); - - // start the sim - sim.start(); - loop { - sim.get_sim_output(); - rate_sleeper.sleep(); - } -} - -#[cfg(feature = "gui")] -pub fn start_gui() { - use crate::gui; - let native_options = eframe::NativeOptions::default(); - let _ = eframe::run_native( - "Mission Control", - native_options, - Box::new(|cc| Box::new(gui::Shell::new(cc))), - ); -} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..0619b43 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,89 @@ +use bevy::prelude::*; +use serde::Deserialize; +use log::{info, error}; + +use crate::simulation::balloon::MaterialType; +use crate::simulation::gas::GasSpecies; +use std::{fs, path::Path}; + +pub struct ConfigPlugin; + +impl Plugin for ConfigPlugin { + fn build(&self, app: &mut App) { + // Initialize default configurations + app.init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::(); + + // Add a system to load configuration from file + // app.add_systems(Startup, load_config); + } +} + +#[derive(Clone, Default, Deserialize, PartialEq, Resource)] +pub struct EnvConfig { + pub real_time: bool, + pub tick_rate_hz: f32, + pub max_elapsed_time_s: f32, + pub initial_altitude_m: f32, + pub initial_velocity_m_s: f32, +} + +#[derive(Clone, Default, Deserialize, PartialEq, Resource)] +pub struct BalloonConfig { + /// Balloon material type + pub material: MaterialType, + /// Thickness of balloon membrane in meters + pub thickness_m: f32, + /// Diameter of "unstressed" balloon membrane when filled, assuming balloon is a sphere, in meters + pub barely_inflated_diameter_m: f32, + /// Configuration for the lift gas + pub lift_gas: GasConfig, +} + +#[derive(Clone, Default, Deserialize, PartialEq, Resource)] +pub struct GasConfig { + /// Species of the gas + pub species: GasSpecies, + /// Mass of the gas in kilograms + pub mass_kg: f32, +} + +#[derive(Clone, Default, Deserialize, PartialEq, Resource)] +pub struct BusConfig { + /// Configuration for the body of the bus + pub body: BodyConfig, + /// Configuration for the parachute system + pub parachute: ParachuteConfig, +} + +#[derive(Copy, Clone, Default, Deserialize, PartialEq, Resource)] +pub struct BodyConfig { + /// Mass of all components less ballast material, in kilograms + pub mass_kg: f32, + /// Effective area used for drag calculations during freefall, in square meters + pub drag_area_m2: f32, + /// Drag coefficient of the payload during freefall + pub drag_coeff: f32, +} + +#[derive(Copy, Clone, Default, Deserialize, PartialEq, Resource)] +pub struct ParachuteConfig { + /// Mass of the parachute system (main + drogue), in kilograms + pub total_mass_kg: f32, + /// Drogue parachute effective area used for drag calculations, in square meters + pub drogue_area_m2: f32, + /// Drogue parachute drag coefficient + pub drogue_drag_coeff: f32, + /// Main parachute effective area used for drag calculations, in square meters + pub main_area_m2: f32, + /// Main parachute drag coefficient when fully deployed + pub main_drag_coeff: f32, + /// Force needed for the drogue to deploy the main chute, in Newtons + pub deploy_force_n: f32, + /// Duration the main chute stays in the partially open state, in seconds + pub deploy_time_s: f32, +} diff --git a/src/gui/mod.rs b/src/gui/mod.rs index f228dc4..ccf4c2f 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,8 +1,35 @@ +use bevy::app::PluginGroupBuilder; +use bevy::prelude::*; + mod shell; -mod views; +// Import other UI-related modules here +// mod config_view; +// mod flight_view; +// mod stats_view; + +use shell::ShellPlugin; +// Use other UI-related plugins here +// use config_view::ConfigViewPlugin; +// use flight_view::FlightViewPlugin; +// use stats_view::StatsViewPlugin; +// Re-export ShellPlugin and other UI-related items if needed pub use shell::Shell; +/// A plugin group that includes all interface-related plugins +pub struct InterfacePlugins; + +impl PluginGroup for InterfacePlugins { + fn build(self) -> PluginGroupBuilder { + PluginGroupBuilder::start::() + .add(ShellPlugin) + // Add other UI-related plugins here + // .add(ConfigViewPlugin) + // .add(FlightViewPlugin) + // .add(StatsViewPlugin) + } +} + /// Something to view in the monitor windows pub trait View { fn ui(&mut self, ui: &mut egui::Ui); diff --git a/src/gui/shell.rs b/src/gui/shell.rs index f10ea15..d79e643 100644 --- a/src/gui/shell.rs +++ b/src/gui/shell.rs @@ -1,26 +1,22 @@ -use std::{ - collections::BTreeSet, - path::PathBuf, - sync::{Arc, Mutex}, - thread::JoinHandle, -}; -use egui::{Context, Modifiers, ScrollArea, Ui}; +use bevy::prelude::*; +use bevy_egui::{egui, EguiContext, EguiPlugin}; use super::UiPanel; -use crate::simulator::{ - io::SimOutput, - config::Config, - schedule::AsyncSim, -}; -/// A menu bar in which you can select different info windows to show. -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "serde", serde(default))] +pub struct ShellPlugin; + +impl Plugin for ShellPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(EguiPlugin) + .init_resource::() + .add_systems(Update, ui_system); + } +} + +#[derive(Resource)] pub struct Shell { screens: Screens, - config: Option, - output: Option, - run_handle: Option>, + config: Option, } impl Default for Shell { @@ -28,12 +24,57 @@ impl Default for Shell { Self { screens: Screens::default(), config: None, - output: None, + output: None, run_handle: None, } } } +fn ui_system(mut egui_context: ResMut, mut shell: ResMut) { + egui::SidePanel::left("mission_control_panel") + .resizable(false) + .default_width(150.0) + .show(egui_context.ctx_mut(), |ui| { + ui.vertical_centered(|ui| { + ui.heading("yahs"); + }); + + ui.separator(); + + use egui::special_emojis::GITHUB; + ui.hyperlink_to( + format!("{GITHUB} yahs on GitHub"), + "https://github.com/brickworks/yahs", + ); + ui.hyperlink_to( + format!("{GITHUB} @philiplinden"), + "https://github.com/philiplinden", + ); + + ui.separator(); + egui::widgets::global_dark_light_mode_buttons(ui); + ui.separator(); + shell.screen_list_ui(ui); + ui.separator(); + shell.sim_control_buttons(ui); + ui.separator(); + }); + + egui::TopBottomPanel::top("menu_bar").show(egui_context.ctx_mut(), |ui| { + egui::menu::bar(ui, |ui| { + file_menu_button(ui); + }); + }); + + shell.show_windows(egui_context.ctx_mut()); + + egui::TopBottomPanel::bottom("powered_by_bevy_egui").show(egui_context.ctx_mut(), |ui| { + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { + powered_by_egui_and_bevy(ui); + }); + }); +} + impl Shell { /// Called once before the first frame. pub fn new(_cc: &eframe::CreationContext<'_>) -> Self { @@ -97,46 +138,79 @@ impl Shell { fn sim_control_buttons(&mut self, ui: &mut egui::Ui) { if ui.button("Simulate").clicked() { - if self.run_handle.is_some() { panic!("Can't start again, sim already ran. Need to stop.") } let outpath = PathBuf::from("out.csv"); let init_state = Arc::new(Mutex::new(SimOutput::default())); if let Some(config) = self.config.clone() { - AsyncSim::run_sim(config, init_state, outpath) + let output = init_state.clone(); + self.run_handle = Some(std::thread::spawn(move || { + AsyncSim::run_sim(config, output, outpath); + })); } } } } -impl eframe::App for Shell { - /// Called each time the UI needs repainting, which may be many times per second. - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - // Put your widgets into a `SidePanel`, `TopBottomPanel`, `CentralPanel`, `Window` or `Area`. - // For inspiration and more examples, go to https://emilk.github.io/egui - egui::CentralPanel::default().show(ctx, |_ui| { - // The central panel the region left after adding TopPanel's and SidePanel's - self.ui(ctx); - }); - egui::TopBottomPanel::bottom("powered_by_eframe").show(ctx, |ui| { - ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { - powered_by_egui_and_eframe(ui); - egui::warn_if_debug_build(ui); - }); - }); +// ---------------------------------------------------------------------------- + +#[derive(Default)] +struct Screens { + screens: Vec>, + open: BTreeSet, +} + +impl Screens { + pub fn from_demos(screens: Vec>) -> Self { + let mut open = BTreeSet::new(); + open.insert(super::views::ConfigView::default().name().to_owned()); + + Self { screens, open } + } + + pub fn checkboxes(&mut self, ui: &mut Ui) { + let Self { screens, open } = self; + for screen in screens { + if screen.is_enabled(ui.ctx()) { + let mut is_open = open.contains(screen.name()); + ui.toggle_value(&mut is_open, screen.name()); + set_open(open, screen.name(), is_open); + } + } + } + + pub fn windows(&mut self, ctx: &Context) { + let Self { screens, open } = self; + for screen in screens { + let mut is_open = open.contains(screen.name()); + screen.show(ctx, &mut is_open); + set_open(open, screen.name(), is_open); + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +fn set_open(open: &mut BTreeSet, key: &'static str, is_open: bool) { + if is_open { + if !open.contains(key) { + open.insert(key.to_owned()); + } + } else { + open.remove(key); } } -fn powered_by_egui_and_eframe(ui: &mut egui::Ui) { +fn powered_by_egui_and_bevy(ui: &mut egui::Ui) { ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 0.0; ui.label("Powered by "); ui.hyperlink_to("egui", "https://github.com/emilk/egui"); ui.label(" and "); ui.hyperlink_to( - "eframe", - "https://github.com/emilk/egui/tree/master/crates/eframe", + "bevy", + "https://github.com/bevyengine/bevy", ); ui.label("."); }); diff --git a/src/gui/views.rs b/src/gui/views.rs index b537d7d..cc65201 100644 --- a/src/gui/views.rs +++ b/src/gui/views.rs @@ -4,7 +4,7 @@ use egui_plot::{ }; use log::error; use crate::gui::View; -use crate::simulator::config::{self, Config}; +use crate::original::config::{self, Config}; // ---------------------------------------------------------------------------- diff --git a/src/main.rs b/src/main.rs index 976aacd..5f1ef56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,30 @@ -mod cli; +mod gui; +mod config; mod simulator; -#[cfg(feature = "gui")] -mod gui; +use bevy::prelude::*; fn main() { + setup_pretty_logs(); + App::new() + .add_plugins(( + DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "🎈".to_string(), + ..default() + }), + ..default() + }), + gui::InterfacePlugins, + config::ConfigPlugin, + )) + .run(); +} + +fn setup_pretty_logs() { // look for the RUST_LOG env var or default to "info" let rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_owned()); std::env::set_var("RUST_LOG", rust_log); // initialize pretty print logger pretty_env_logger::init(); - // parse the commands, arguments, and options - cli::parse_inputs(); } diff --git a/src/simulator/balloon.rs b/src/simulator/balloon.rs index 9ce2f40..9eaac30 100644 --- a/src/simulator/balloon.rs +++ b/src/simulator/balloon.rs @@ -4,9 +4,6 @@ // Properties, attributes and functions related to the balloon. // ---------------------------------------------------------------------------- -#![allow(dead_code)] - -extern crate libm; use log::debug; use serde::Deserialize; diff --git a/src/simulator/config.rs b/src/simulator/config.rs deleted file mode 100644 index 55d8e84..0000000 --- a/src/simulator/config.rs +++ /dev/null @@ -1,90 +0,0 @@ -use serde::Deserialize; -use log::{info, error}; - -use super::balloon::MaterialType; -use super::gas::GasSpecies; -use std::{fs, path::Path}; - -pub fn parse_from_file>(path: P) -> Config { - // Read the contents of the configuration file as string - match fs::read_to_string(path) { - // If successful return the files text as `contents`. - // `c` is a local variable. - Ok(contents) => parse(&contents), - // Handle the `error` case. - Err(_) => { - // Write `msg` to `stderr`. - error!("Could not read file!"); - Config::default() - } - } -} - -pub fn parse(contents: &String) -> Config { - // unpack the config TOML from string - match toml::from_str(contents) { - Ok(parsed) => { - info!("Parsed config:\n{:}", contents); - parsed - }, - Err(_) => { - error!("Could not parse config! Using defaults."); - Config::default() - } - } -} - -#[derive(Clone, Default, Deserialize, PartialEq)] -pub struct Config { - pub environment: EnvConfig, - pub balloon: BalloonConfig, - pub bus: BusConfig, -} - -#[derive(Clone, Default, Deserialize, PartialEq)] -pub struct EnvConfig { - pub real_time: bool, - pub tick_rate_hz: f32, - pub max_elapsed_time_s: f32, - pub initial_altitude_m: f32, - pub initial_velocity_m_s: f32, -} - -#[derive(Clone, Default, Deserialize, PartialEq)] -pub struct BalloonConfig { - pub material: MaterialType, // balloon material - pub thickness_m: f32, // thickness of balloon membrane - pub barely_inflated_diameter_m: f32, // assuming balloon is a sphere, diameter of "unstressed" balloon membrane when filled - pub lift_gas: GasConfig, -} - -#[derive(Clone, Default, Deserialize, PartialEq)] -pub struct GasConfig { - pub species: GasSpecies, - pub mass_kg: f32, -} - -#[derive(Clone, Default, Deserialize, PartialEq)] -pub struct BusConfig { - pub body: BodyConfig, - pub parachute: ParachuteConfig, - // pub control: ControlConfig, -} - -#[derive(Copy, Clone, Default, Deserialize, PartialEq)] -pub struct BodyConfig { - pub mass_kg: f32, // mass of all components less ballast material - pub drag_area_m2: f32, // effective area used for drag calculations during freefall - pub drag_coeff: f32, // drag coefficient of the payload during freefall -} - -#[derive(Copy, Clone, Default, Deserialize, PartialEq)] -pub struct ParachuteConfig { - pub total_mass_kg: f32, // mass of the parachute system (main + drogue) - pub drogue_area_m2: f32, // drogue parachute effective area used for drag calculations - pub drogue_drag_coeff: f32, // drogue parachute drag coefficient - pub main_area_m2: f32, // main parachute effective area used for drag calculations - pub main_drag_coeff: f32, // main parachute drag coefficient when fully deployed - pub deploy_force_n: f32, // force needed for the drogue to deploy the main chute - pub deploy_time_s: f32, // how long the main chute stays in the partially open state -} diff --git a/src/simulator/mod.rs b/src/simulator/mod.rs index ae1ea6b..d6c2d9e 100644 --- a/src/simulator/mod.rs +++ b/src/simulator/mod.rs @@ -1,10 +1,10 @@ -mod balloon; -mod bus; +pub mod balloon; +pub mod bus; pub mod config; -mod constants; -mod forces; -mod gas; -mod heat; +pub mod constants; +pub mod forces; +pub mod gas; +pub mod heat; pub mod io; pub mod schedule; From 22a6f807e50ff37bb9cfd39831095a3997660954 Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Thu, 7 Nov 2024 20:13:29 -0500 Subject: [PATCH 03/47] bevy: round 1 --- Cargo.toml | 7 +- {config => assets}/default.toml | 0 assets/gases.ron | 60 +++++++ assets/materials.ron | 43 +++++ docs/devlog.md | 37 +++++ src/config.rs | 44 +---- src/main.rs | 5 +- src/simulator/balloon.rs | 166 ++++++++----------- src/simulator/bus.rs | 1 - src/simulator/forces.rs | 8 +- src/simulator/gas.rs | 173 ++++++++------------ src/simulator/io.rs | 64 -------- src/simulator/mod.rs | 16 +- src/simulator/schedule.rs | 281 ++------------------------------ 14 files changed, 317 insertions(+), 588 deletions(-) rename {config => assets}/default.toml (100%) create mode 100644 assets/gases.ron create mode 100644 assets/materials.ron create mode 100644 docs/devlog.md delete mode 100644 src/simulator/io.rs diff --git a/Cargo.toml b/Cargo.toml index 85dcf73..aee697e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,19 +7,16 @@ edition = "2021" readme = "README.md" license-file = "LICENSE" -[features] -default = [] - [dependencies] pretty_env_logger = "0.5.0" -serde = { version = "1.0.196", features = ["derive"] } +serde = { version = "1.0.214", features = ["derive"] } log = { version = "0.4.20", features = ["release_max_level_debug"] } -egui = { version = "0.29.1", features = ["log", "serde"] } egui_plot = { version = "0.29.0", features = ["serde"] } egui_extras = { version = "0.29.1", features = ["file"] } bevy = "0.14.2" bevy_egui = "0.30.0" avian3d = "0.1.2" +ron = "0.8.1" [[bin]] name = "yahs" diff --git a/config/default.toml b/assets/default.toml similarity index 100% rename from config/default.toml rename to assets/default.toml diff --git a/assets/gases.ron b/assets/gases.ron new file mode 100644 index 0000000..4e3d83d --- /dev/null +++ b/assets/gases.ron @@ -0,0 +1,60 @@ +( + gases: [ + // Species of gas with a known molar mass (kg/mol) + ( + name: "Air", + abbreviation: "Air", + molar_mass: 0.02897, + ), + ( + name: "Helium", + abbreviation: "He", + molar_mass: 0.0040026, + ), + ( + name: "Hydrogen", + abbreviation: "H2", + molar_mass: 0.00201594, + ), + ( + name: "Nitrogen", + abbreviation: "N2", + molar_mass: 0.0280134, + ), + ( + name: "Oxygen", + abbreviation: "O2", + molar_mass: 0.0319988, + ), + ( + name: "Argon", + abbreviation: "Ar", + molar_mass: 0.039948, + ), + ( + name: "Carbon Dioxide", + abbreviation: "CO2", + molar_mass: 0.04400995, + ), + ( + name: "Neon", + abbreviation: "Ne", + molar_mass: 0.020183, + ), + ( + name: "Krypton", + abbreviation: "Kr", + molar_mass: 0.08380, + ), + ( + name: "Xenon", + abbreviation: "Xe", + molar_mass: 0.13130, + ), + ( + name: "Methane", + abbreviation: "CH4", + molar_mass: 0.01604303, + ), + ] +) diff --git a/assets/materials.ron b/assets/materials.ron new file mode 100644 index 0000000..eedb905 --- /dev/null +++ b/assets/materials.ron @@ -0,0 +1,43 @@ +( + materials: [ + ( + name: "Nothing", + max_temperature: Infinity, + density: 0.0, + emissivity: 1.0, + absorptivity: 0.0, + thermal_conductivity: Infinity, + specific_heat: 0.0, + poissons_ratio: 0.5, + elasticity: Infinity, + max_strain: Infinity, + max_stress: Infinity, + ), + ( + name: "Rubber", + max_temperature: 385.0, + density: 1000.0, + emissivity: 0.86, + absorptivity: 0.86, + thermal_conductivity: 0.25, + specific_heat: 1490.0, + poissons_ratio: 0.5, + elasticity: 4_000_000.0, + max_strain: 8.0, + max_stress: 25_000_000.0, + ), + ( + name: "LowDensityPolyethylene", + max_temperature: 348.0, + density: 919.0, + emissivity: 0.94, + absorptivity: 0.94, + thermal_conductivity: 0.3175, + specific_heat: 2600.0, + poissons_ratio: 0.5, + elasticity: 300_000_000.0, + max_strain: 6.25, + max_stress: 10_000_000.0, + ), + ] +) diff --git a/docs/devlog.md b/docs/devlog.md new file mode 100644 index 0000000..004f4f1 --- /dev/null +++ b/docs/devlog.md @@ -0,0 +1,37 @@ +# development log + +## 2024-11-07 +I am switching to Bevy for the simulation. Bevy is a "bevy engine" which is A +framework for building games and simulations. It allows for high performance, +multi-threaded, dynamic simulations. + +The first reason is that I want to be able to spend more time on the +interactions between the HAB components and less time on the fundamental physics +and simulation scheduling loop. Bevy has a very nice schedule system that allows +for easy parallelization of systems. It also has a component system that allows +me to keep all the logic for the physics systems close to the objects that they +act on. For example, all of the solid bodies that will need to have drag applied +will have a `Body` component, and the logic to calculate the drag on those +bodies will be computed from the their mesh using custom colliders and forces on +top of the physics engine, [Avian](https://github.com/Jondolf/avian), that takes +care of equations of motion, collisions, and constraints. + +The second reason is that I want to be able to run the simulation in a web +browser. Bevy has a web backend that allows for this and very nice tools for +visualizing the simulation state. It also has first-class support for +[Egui](https://github.com/emilk/egui) which is a library for building +interactive GUIs with [Bevy Egui](https://github.com/mvlabat/bevy_egui), and +first-class support for loading assets like configs, 3D models, and textures. + +The first thing I want to do is to get a simple ballistic flight working in +Bevy so that I can validate the fundamental physics assumptions that I have +made. To do this I'll need to start duplicating some of the functionality that +I had in the previous Rust simulation. Namely, I need to: + +1. Initialize the atmosphere. +2. Create the solid bodies (balloon, parachute, payload). +3. Set up a schedule for computing the physics updates every tick/frame. + +The Bevy schedule system will completely replace the threaded event loop that I +had in the previous simulation, including things like `SimCommands`, +`SimOutput`, `SimInstant`, and the `AsyncSim` struct. diff --git a/src/config.rs b/src/config.rs index 0619b43..3489e3f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,38 +1,4 @@ -use bevy::prelude::*; -use serde::Deserialize; -use log::{info, error}; - -use crate::simulation::balloon::MaterialType; -use crate::simulation::gas::GasSpecies; -use std::{fs, path::Path}; - -pub struct ConfigPlugin; - -impl Plugin for ConfigPlugin { - fn build(&self, app: &mut App) { - // Initialize default configurations - app.init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::(); - - // Add a system to load configuration from file - // app.add_systems(Startup, load_config); - } -} - -#[derive(Clone, Default, Deserialize, PartialEq, Resource)] -pub struct EnvConfig { - pub real_time: bool, - pub tick_rate_hz: f32, - pub max_elapsed_time_s: f32, - pub initial_altitude_m: f32, - pub initial_velocity_m_s: f32, -} - -#[derive(Clone, Default, Deserialize, PartialEq, Resource)] +#[derive(Component)] pub struct BalloonConfig { /// Balloon material type pub material: MaterialType, @@ -44,7 +10,7 @@ pub struct BalloonConfig { pub lift_gas: GasConfig, } -#[derive(Clone, Default, Deserialize, PartialEq, Resource)] +#[derive(Component)] pub struct GasConfig { /// Species of the gas pub species: GasSpecies, @@ -52,7 +18,7 @@ pub struct GasConfig { pub mass_kg: f32, } -#[derive(Clone, Default, Deserialize, PartialEq, Resource)] +#[derive(Component)] pub struct BusConfig { /// Configuration for the body of the bus pub body: BodyConfig, @@ -60,7 +26,7 @@ pub struct BusConfig { pub parachute: ParachuteConfig, } -#[derive(Copy, Clone, Default, Deserialize, PartialEq, Resource)] +#[derive(Component)] pub struct BodyConfig { /// Mass of all components less ballast material, in kilograms pub mass_kg: f32, @@ -70,7 +36,7 @@ pub struct BodyConfig { pub drag_coeff: f32, } -#[derive(Copy, Clone, Default, Deserialize, PartialEq, Resource)] +#[derive(Component)] pub struct ParachuteConfig { /// Mass of the parachute system (main + drogue), in kilograms pub total_mass_kg: f32, diff --git a/src/main.rs b/src/main.rs index 5f1ef56..5f4538d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -mod gui; +// mod gui; mod config; mod simulator; @@ -15,8 +15,7 @@ fn main() { }), ..default() }), - gui::InterfacePlugins, - config::ConfigPlugin, + // gui::InterfacePlugins, )) .run(); } diff --git a/src/simulator/balloon.rs b/src/simulator/balloon.rs index 9eaac30..d6f7ba2 100644 --- a/src/simulator/balloon.rs +++ b/src/simulator/balloon.rs @@ -9,14 +9,16 @@ use log::debug; use serde::Deserialize; use std::f32::consts::PI; use std::fmt; +use std::fs; +use ron::de::from_str; use super::{gas, SolidBody}; -#[derive(Copy, Clone)] -pub struct Balloon { +#[derive(Clone)] +pub struct Balloon<'a> { pub intact: bool, // whether or not it has burst - pub lift_gas: gas::GasVolume, // gas inside the balloon - pub material: Material, // what the balloon is made of + pub lift_gas: gas::GasVolume<'a>, // gas inside the balloon + pub material: BalloonMaterial, // what the balloon is made of pub skin_thickness: f32, // thickness of the skin of the balloon (m) mass: f32, // balloon mass (kg) drag_coeff: f32, // drag coefficient @@ -27,12 +29,12 @@ pub struct Balloon { strain: f32, } -impl Balloon { +impl<'a> Balloon<'a> { pub fn new( - material: Material, // material of balloon skin + material: BalloonMaterial, // material of balloon skin skin_thickness: f32, // balloon skin thickness (m) at zero pressure barely_inflated_diameter: f32, // internal diameter (m) at zero pressure - lift_gas: gas::GasVolume, // species of gas inside balloon + lift_gas: gas::GasVolume<'a>, // species of gas inside balloon ) -> Self { let unstretched_radius = barely_inflated_diameter / 2.0; let mass = shell_volume(unstretched_radius, skin_thickness) * material.density; @@ -51,15 +53,15 @@ impl Balloon { } } - pub fn surface_area(self) -> f32 { + pub fn surface_area(&self) -> f32 { sphere_surface_area(sphere_radius_from_volume(self.lift_gas.volume())) } - pub fn radius(self) -> f32 { + pub fn radius(&self) -> f32 { sphere_radius_from_volume(self.volume()) } - pub fn volume(self) -> f32 { + pub fn volume(&self) -> f32 { self.lift_gas.volume() } @@ -67,7 +69,7 @@ impl Balloon { self.lift_gas.set_volume(new_volume) } - pub fn pressure(&mut self) -> f32 { + pub fn pressure(&self) -> f32 { self.lift_gas.pressure() } @@ -79,11 +81,11 @@ impl Balloon { self.skin_thickness = new_thickness } - fn gage_pressure(self, external_pressure: f32) -> f32 { + pub fn gage_pressure(&self, external_pressure: f32) -> f32 { self.lift_gas.pressure() - external_pressure } - pub fn stress(self) -> f32 { + pub fn stress(&self) -> f32 { self.stress } @@ -100,7 +102,7 @@ impl Balloon { } } - pub fn strain(self) -> f32 { + pub fn strain(&self) -> f32 { self.strain } @@ -118,23 +120,23 @@ impl Balloon { } } - fn radial_displacement(self, external_pressure: f32) -> f32 { + pub fn radial_displacement(&self, external_pressure: f32) -> f32 { // https://pkel015.connect.amazon.auckland.ac.nz/SolidMechanicsBooks/Part_I/BookSM_Part_I/07_ElasticityApplications/07_Elasticity_Applications_03_Presure_Vessels.pdf ((1.0 - self.material.poissons_ratio) / self.material.elasticity) - * ((self.gage_pressure(external_pressure) * libm::powf(self.radius(), 2.0)) / 2.0 + * ((self.gage_pressure(external_pressure) * f32::powf(self.radius(), 2.0)) / 2.0 * self.skin_thickness) } fn rebound(&mut self, radial_displacement: f32) -> f32 { // https://physics.stackexchange.com/questions/10372/inflating-a-balloon-expansion-resistance self.set_thickness( - self.unstretched_thickness * libm::powf(self.unstretched_radius / self.radius(), 2.0), + self.unstretched_thickness * f32::powf(self.unstretched_radius / self.radius(), 2.0), ); 2.0 * self.material.elasticity * radial_displacement * self.unstretched_thickness * self.unstretched_radius - / libm::powf(self.radius(), 3.0) + / f32::powf(self.radius(), 3.0) } pub fn stretch(&mut self, external_pressure: f32) { @@ -182,7 +184,7 @@ impl Balloon { } } -impl SolidBody for Balloon { +impl<'a> SolidBody for Balloon<'a> { fn drag_area(&self) -> f32 { if self.intact { sphere_radius_from_volume(self.volume()) @@ -201,7 +203,7 @@ impl SolidBody for Balloon { } fn sphere_volume(radius: f32) -> f32 { - (4.0 / 3.0) * PI * libm::powf(radius, 3.0) + (4.0 / 3.0) * PI * f32::powf(radius, 3.0) } fn shell_volume(internal_radius: f32, thickness: f32) -> f32 { @@ -212,16 +214,16 @@ fn shell_volume(internal_radius: f32, thickness: f32) -> f32 { } fn sphere_radius_from_volume(volume: f32) -> f32 { - libm::powf(volume, 1.0 / 3.0) / (4.0 / 3.0) * PI + f32::powf(volume, 1.0 / 3.0) / (4.0 / 3.0) * PI } fn sphere_surface_area(radius: f32) -> f32 { - 4.0 * PI * libm::powf(radius, 2.0) + 4.0 * PI * f32::powf(radius, 2.0) } pub fn projected_spherical_area(volume: f32) -> f32 { - // Get the projected area (m^2) of a sphere with a given volume (m^3) - libm::powf(sphere_radius_from_volume(volume), 2.0) * PI + // Get the projected area (m^2) of a sphere with a given volume (m³) + f32::powf(sphere_radius_from_volume(volume), 2.0) * PI } // ---------------------------------------------------------------------------- @@ -231,10 +233,47 @@ pub fn projected_spherical_area(volume: f32) -> f32 { // Source: https://www.matweb.com/ // ---------------------------------------------------------------------------- -#[derive(Copy, Clone, PartialEq)] -pub struct Material { +#[derive(Debug, Deserialize, Clone)] +pub struct MaterialConfig { + materials: Vec, +} + +impl BalloonMaterial { + pub fn load_materials() -> Vec { + let content = fs::read_to_string("path/to/materials.ron").expect("Unable to read file"); + let config: MaterialConfig = from_str(&content).expect("Unable to parse RON"); + config.materials + } + + pub fn new(material_name: &str) -> Self { + let materials = BalloonMaterial::load_materials(); + materials.into_iter().find(|m| m.name == material_name).unwrap_or_default() + } +} + +impl Default for BalloonMaterial { + fn default() -> Self { + BalloonMaterial { + name: "Latex".to_string(), + max_temperature: 373.0, // Example value in Kelvin + density: 920.0, // Example density in kg/m³ + emissivity: 0.9, // Example emissivity + absorptivity: 0.9, // Example absorptivity + thermal_conductivity: 0.13, // Example thermal conductivity in W/mK + specific_heat: 2000.0, // Example specific heat in J/kgK + poissons_ratio: 0.5, // Example Poisson's ratio + elasticity: 0.01e9, // Example Young's Modulus in Pa + max_strain: 0.8, // Example max strain (unitless) + max_stress: 0.5e6, // Example max stress in Pa + } + } +} + +#[derive(Debug, Deserialize, Clone, PartialEq)] +pub struct BalloonMaterial { + pub name: String, pub max_temperature: f32, // temperature (K) where the given material fails - pub density: f32, // density (kg/m^3) + pub density: f32, // density (kg/m³) pub emissivity: f32, // how much thermal radiation is emitted pub absorptivity: f32, // how much thermal radiation is absorbed pub thermal_conductivity: f32, // thermal conductivity (W/mK) of the material at room temperature @@ -245,77 +284,8 @@ pub struct Material { pub max_stress: f32, // tangential stress at failure (Pa) } -impl Material { - pub fn new(material_type: MaterialType) -> Self { - match material_type { - MaterialType::Rubber => RUBBER, - MaterialType::LDPE | MaterialType::LowDensityPolyethylene => LOW_DENSITY_POLYETHYLENE, - _ => NOTHING, - } - } -} - -#[derive(Copy, Clone, Default, PartialEq, Deserialize)] -pub enum MaterialType { - // Species of gas with a known molar mass (kg/mol) - Nothing, - #[default] Rubber, - LDPE, - LowDensityPolyethylene, -} - -impl fmt::Display for MaterialType { +impl fmt::Display for BalloonMaterial { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - MaterialType::Nothing => write!(f, "nothing"), - MaterialType::Rubber => write!(f, "rubber"), - MaterialType::LDPE | MaterialType::LowDensityPolyethylene => { - write!(f, "low-density polyethylene (LDPE)") - } - } + write!(f, "{}", self.name) } } - -pub const NOTHING: Material = Material { - // nothing - max_temperature: f32::INFINITY, - density: 0.0, - emissivity: 1.0, - absorptivity: 0.0, - thermal_conductivity: f32::INFINITY, - specific_heat: 0.0, - poissons_ratio: 0.5, - elasticity: f32::INFINITY, - max_strain: f32::INFINITY, - max_stress: f32::INFINITY, -}; - -pub const RUBBER: Material = Material { - // Nitrile Butadiene Rubber - // https://designerdata.nl/materials/plastics/rubbers/nitrile-butadiene-rubber - max_temperature: 385.0, - density: 1000.0, - emissivity: 0.86, - absorptivity: 0.86, - thermal_conductivity: 0.25, - specific_heat: 1490.0, - poissons_ratio: 0.5, - elasticity: 4_000_000.0, - max_strain: 8.0, - max_stress: 25_000_000.0, -}; - -pub const LOW_DENSITY_POLYETHYLENE: Material = Material { - // Low Density Polyethylene (LDPE) - // https://designerdata.nl/materials/plastics/thermo-plastics/low-density-polyethylene - max_temperature: 348.0, - density: 919.0, - emissivity: 0.94, - absorptivity: 0.94, - thermal_conductivity: 0.3175, - specific_heat: 2600.0, - poissons_ratio: 0.5, - elasticity: 300_000_000.0, - max_strain: 6.25, - max_stress: 10_000_000.0, -}; diff --git a/src/simulator/bus.rs b/src/simulator/bus.rs index dfa6631..1e96d9b 100644 --- a/src/simulator/bus.rs +++ b/src/simulator/bus.rs @@ -5,7 +5,6 @@ // ---------------------------------------------------------------------------- use super::{ - config::{BodyConfig, ParachuteConfig}, forces, gas::Atmosphere, SolidBody, diff --git a/src/simulator/forces.rs b/src/simulator/forces.rs index b39f390..effb24d 100644 --- a/src/simulator/forces.rs +++ b/src/simulator/forces.rs @@ -5,10 +5,6 @@ // coordinate frame and aR_E R_Eported in Newtons. // ---------------------------------------------------------------------------- -#![allow(dead_code)] - -extern crate libm; - use super::constants::{EARTH_RADIUS_M, STANDARD_G}; use super::{gas, SolidBody}; @@ -36,8 +32,8 @@ pub fn buoyancy(altitude: f32, atmo: gas::Atmosphere, lift_gas: gas::GasVolume) pub fn drag(atmo: gas::Atmosphere, velocity: f32, body: T) -> f32 { // Force (N) due to drag against the balloon - let direction = -libm::copysignf(1.0, velocity); - direction * body.drag_coeff() / 2.0 * atmo.density() * libm::powf(velocity, 2.0) * body.drag_area() + let direction = -f32::copysign(1.0, velocity); + direction * body.drag_coeff() / 2.0 * atmo.density() * f32::powf(velocity, 2.0) * body.drag_area() } pub fn gross_lift(atmo: gas::Atmosphere, lift_gas: gas::GasVolume) -> f32 { diff --git a/src/simulator/gas.rs b/src/simulator/gas.rs index 4816ad9..b94fac4 100644 --- a/src/simulator/gas.rs +++ b/src/simulator/gas.rs @@ -20,86 +20,50 @@ use super::constants::{R, STANDARD_PRESSURE, STANDARD_TEMPERATURE}; use log::error; +use ron::de::from_str; use serde::Deserialize; use std::fmt; +use std::fs; -#[derive(Copy, Clone, Default, Deserialize, PartialEq)] -pub enum GasSpecies { - // Species of gas with a known molar mass (kg/mol) - Air, - He, - #[default] Helium, - H2, - Hydrogen, - N2, - Nitrogen, - O2, - Oxygen, - Ar, - Argon, - CO2, - CarbonDioxide, - Ne, - Neon, - Kr, - Krypton, - Xe, - Xenon, - CH4, - Methane, +#[derive(Debug, Deserialize, Clone)] +pub struct GasSpecies { + pub name: String, + pub abbreviation: String, + pub molar_mass: f32, // [kg/mol] molar mass a.k.a. molecular weight } -impl fmt::Display for GasSpecies { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - GasSpecies::Air => write!(f, "Air"), - GasSpecies::He => write!(f, "Helium"), - GasSpecies::Helium => write!(f, "Helium"), - GasSpecies::H2 => write!(f, "Hydrogen"), - GasSpecies::Hydrogen => write!(f, "Hydrogen"), - GasSpecies::N2 => write!(f, "Nitrogen"), - GasSpecies::Nitrogen => write!(f, "Nitrogen"), - GasSpecies::O2 => write!(f, "Oxygen"), - GasSpecies::Oxygen => write!(f, "Oxygen"), - GasSpecies::Ar => write!(f, "Argon"), - GasSpecies::Argon => write!(f, "Argon"), - GasSpecies::CO2 => write!(f, "Carbon Dioxide"), - GasSpecies::CarbonDioxide => write!(f, "Carbon Dioxide"), - GasSpecies::Ne => write!(f, "Neon"), - GasSpecies::Neon => write!(f, "Neon"), - GasSpecies::Kr => write!(f, "Krypton"), - GasSpecies::Krypton => write!(f, "Krypton"), - GasSpecies::Xe => write!(f, "Xenon"), - GasSpecies::Xenon => write!(f, "Xenon"), - GasSpecies::CH4 => write!(f, "Methane"), - GasSpecies::Methane => write!(f, "Methane"), +impl GasSpecies { + pub fn new(name: String, abbreviation: String, molar_mass: f32) -> Self { + GasSpecies { + name, + abbreviation, + molar_mass, } } } -#[derive(Copy, Clone)] -pub struct GasVolume { +#[derive(Debug, Clone, Copy)] +pub struct GasVolume<'a> { // A finite amount of a particular gas - species: GasSpecies, // type of gas - mass: f32, // [kg] amount of gas in the volume - temperature: f32, // [K] temperature - pressure: f32, // [Pa] pressure - molar_mass: f32, // [kg/mol] molar mass a.k.a. molecular weight - volume: f32, // [m^3] volume + species: &'a GasSpecies, // Reference to the type of gas + mass: f32, // [kg] amount of gas in the volume + temperature: f32, // [K] temperature + pressure: f32, // [Pa] pressure + volume: f32, // [m³] volume } -impl fmt::Display for GasVolume { +impl<'a> fmt::Display for GasVolume<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "{:}: {:} kg | {:} K | {:} Pa | {:} m^3", - self.species, self.mass, self.temperature, self.pressure, self.volume, + "{:}: {:} kg | {:} K | {:} Pa | {:} m³", + self.species.name, self.mass, self.temperature, self.pressure, self.volume, ) } } -impl GasVolume { - pub fn new(species: GasSpecies, mass: f32) -> Self { +impl<'a> GasVolume<'a> { + pub fn new(species: &'a GasSpecies, mass: f32) -> Self { // Create a new gas volume as a finite amount of mass (kg) of a // particular species of gas. Gas is initialized at standard // temperature and pressure. @@ -108,12 +72,11 @@ impl GasVolume { mass, temperature: STANDARD_TEMPERATURE, pressure: STANDARD_PRESSURE, - molar_mass: molar_mass(species), volume: volume( STANDARD_TEMPERATURE, STANDARD_PRESSURE, mass, - molar_mass(species), + species.molar_mass, // Accessing molar mass through the reference ), } } @@ -134,10 +97,11 @@ impl GasVolume { } pub fn set_volume(&mut self, new_volume: f32) { - // set the volume (m^3) of the GasVolume and update the pressure + // set the volume (m³) of the GasVolume and update the pressure // according to the ideal gas law. self.volume = new_volume; - let new_pressure = ((self.mass / self.molar_mass) * R * self.temperature) / self.volume; + let new_pressure = ((self.mass / self.species.molar_mass) * R * self.temperature) + / self.volume; self.set_pressure(new_pressure); } @@ -151,9 +115,9 @@ impl GasVolume { } pub fn set_mass_from_volume(&mut self) { - // set the mass (kg) based on the current volume (m^3), - // density (kg/m^3), and molar mass (kg/mol) - self.mass = self.volume * (self.molar_mass / R) * (self.pressure / self.temperature); + // set the mass (kg) based on the current volume (m³), + // density (kg/m³), and molar mass (kg/mol) + self.mass = self.volume * (self.species.molar_mass / R) * (self.pressure / self.temperature); } pub fn temperature(self) -> f32 { @@ -171,13 +135,13 @@ impl GasVolume { self.mass } pub fn density(self) -> f32 { - // density (kg/m^3) - density(self.temperature, self.pressure, self.molar_mass) + // density (kg/m³) + density(self.temperature, self.pressure, self.species.molar_mass) } pub fn volume(&self) -> f32 { - // volume (m^3) - volume(self.temperature, self.pressure, self.mass, self.molar_mass) + // volume (m³) + volume(self.temperature, self.pressure, self.mass, self.species.molar_mass) } } @@ -187,7 +151,7 @@ pub struct Atmosphere { altitude: f32, // [m] altitude (which determines the other attributes) temperature: f32, // [K] temperature pressure: f32, // [Pa] pressure - density: f32, // [kg/m^3] density + density: f32, // [kg/m³] density molar_mass: f32, // [kg/mol] molar mass a.k.a. molecular weight } @@ -195,7 +159,7 @@ impl fmt::Display for Atmosphere { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "{:} K | {:} Pa | {:} kg/m^3", + "{:} K | {:} Pa | {:} kg/m³", self.temperature, self.pressure, self.density, ) } @@ -210,9 +174,9 @@ impl Atmosphere { density: density( coesa_temperature(altitude), coesa_pressure(altitude), - molar_mass(GasSpecies::Air), + 28.9647, ), - molar_mass: molar_mass(GasSpecies::Air), + molar_mass: 28.9647, } } pub fn set_altitude(&mut self, new_altitude: f32) { @@ -234,39 +198,33 @@ impl Atmosphere { } pub fn density(self) -> f32 { - // Density (kg/m^3) + // Density (kg/m³) self.density } } +impl Default for Atmosphere { + fn default() -> Self { + Atmosphere { + altitude: 0.0, // Sea level + temperature: STANDARD_TEMPERATURE, // Standard sea level temperature + pressure: STANDARD_PRESSURE, // Standard sea level pressure + density: density(STANDARD_TEMPERATURE, STANDARD_PRESSURE, 28.9647), // Calculate density at sea level + molar_mass: 28.9647, // Molar mass of air + } + } +} + fn volume(temperature: f32, pressure: f32, mass: f32, molar_mass: f32) -> f32 { - // Volume (m^3) of an ideal gas from its temperature (K), pressure (Pa), + // Volume (m³) of an ideal gas from its temperature (K), pressure (Pa), // mass (kg) and molar mass (kg/mol). - (mass / molar_mass) * R * temperature / pressure // [m^3] + (mass / molar_mass) * R * temperature / pressure // [m³] } fn density(temperature: f32, pressure: f32, molar_mass: f32) -> f32 { - // Density (kg/m^3) of an ideal gas frorm its temperature (K), pressure (Pa), + // Density (kg/m³) of an ideal gas frorm its temperature (K), pressure (Pa), // and molar mass (kg/mol) - (molar_mass * pressure) / (R * temperature) // [kg/m^3] -} - -fn molar_mass(species: GasSpecies) -> f32 { - // Get the molecular weight (kg/mol) of a dry gas at sea level. - // Source: US Standard Atmosphere, 1976 - match species { - GasSpecies::Air => 0.02897, - GasSpecies::He | GasSpecies::Helium => 0.0040026, - GasSpecies::H2 | GasSpecies::Hydrogen => 0.00201594, - GasSpecies::N2 | GasSpecies::Nitrogen => 0.0280134, - GasSpecies::O2 | GasSpecies::Oxygen => 0.0319988, - GasSpecies::Ar | GasSpecies::Argon => 0.039948, - GasSpecies::CO2 | GasSpecies::CarbonDioxide => 0.04400995, - GasSpecies::Ne | GasSpecies::Neon => 0.020183, - GasSpecies::Kr | GasSpecies::Krypton => 0.08380, - GasSpecies::Xe | GasSpecies::Xenon => 0.13130, - GasSpecies::CH4 | GasSpecies::Methane => 0.01604303, - } + (molar_mass * pressure) / (R * temperature) // [kg/m³] } fn coesa_temperature(altitude: f32) -> f32 { @@ -293,11 +251,11 @@ fn coesa_pressure(altitude: f32) -> f32 { // Only valid for altitudes below 85,000 meters. // Based on the US Standard Atmosphere, 1976. (aka COESA) if (-57.0..11000.0).contains(&altitude) { - (101.29 * libm::powf(coesa_temperature(altitude) / 288.08, 5.256)) * 1000.0 + (101.29 * f32::powf(coesa_temperature(altitude) / 288.08, 5.256)) * 1000.0 } else if (11000.0..25000.0).contains(&altitude) { - (22.65 * libm::expf(1.73 - 0.000157 * altitude)) * 1000.0 + (22.65 * f32::exp(1.73 - 0.000157 * altitude)) * 1000.0 } else if (25000.0..85000.0).contains(&altitude) { - (2.488 * libm::powf(coesa_temperature(altitude) / 216.6, -11.388)) * 1000.0 + (2.488 * f32::powf(coesa_temperature(altitude) / 216.6, -11.388)) * 1000.0 } else { error!( "Altitude {:}m is outside of the accepted range! Must be 0-85000m", @@ -311,3 +269,14 @@ fn celsius2kelvin(deg_celsius: f32) -> f32 { // Convert degrees C to Kelvin deg_celsius + 273.15 } + +#[derive(Debug, Deserialize)] +struct GasConfig { + gases: Vec, +} + +fn load_gas_config(file_path: &str) -> Vec { + let content = fs::read_to_string(file_path).expect("Unable to read file"); + let config: GasConfig = from_str(&content).expect("Unable to parse RON"); + config.gases +} diff --git a/src/simulator/io.rs b/src/simulator/io.rs deleted file mode 100644 index ab7ac5f..0000000 --- a/src/simulator/io.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::{fs, path::PathBuf}; -fn init_log_file(outpath: PathBuf) -> csv::Writer { - let mut writer = csv::Writer::from_path(outpath).unwrap(); - writer - .write_record(&[ - "time_s", - "altitude_m", - "ascent_rate_m_s", - "acceleration_m_s2", - "atmo_temp_K", - "atmo_pres_Pa", - "balloon_pres_Pa", - "balloon_radius_m", - "balloon_stress_Pa", - "balloon_strain_pct", - "balloon_thickness_m", - "drogue_parachute_area_m2", - "main_parachute_area_m2", - ]) - .unwrap(); - writer -} - -pub fn log_to_file(sim_output: &SimOutput, writer: &mut csv::Writer) { - writer - .write_record(&[ - sim_output.time_s.to_string(), - sim_output.altitude.to_string(), - sim_output.ascent_rate.to_string(), - sim_output.acceleration.to_string(), - sim_output.atmo_temp.to_string(), - sim_output.atmo_pres.to_string(), - sim_output.balloon_pres.to_string(), - sim_output.balloon_radius.to_string(), - sim_output.balloon_stress.to_string(), - sim_output.balloon_strain.to_string(), - sim_output.balloon_thickness.to_string(), - sim_output.drogue_parachute_area.to_string(), - sim_output.main_parachute_area.to_string(), - ]) - .unwrap(); - writer.flush().unwrap(); -} - -#[derive(Default, Copy, Clone)] -pub struct SimOutput { - pub time_s: f32, - pub altitude: f32, - pub ascent_rate: f32, - pub acceleration: f32, - pub atmo_temp: f32, - pub atmo_pres: f32, - pub balloon_pres: f32, - pub balloon_radius: f32, - pub balloon_stress: f32, - pub balloon_strain: f32, - pub balloon_thickness: f32, - pub drogue_parachute_area: f32, - pub main_parachute_area: f32, -} - -pub struct SimCommands { - //TODO: add ability to inject commands to logic controllers -} diff --git a/src/simulator/mod.rs b/src/simulator/mod.rs index d6c2d9e..5a78c37 100644 --- a/src/simulator/mod.rs +++ b/src/simulator/mod.rs @@ -1,15 +1,23 @@ pub mod balloon; -pub mod bus; -pub mod config; +// pub mod bus; pub mod constants; pub mod forces; pub mod gas; -pub mod heat; -pub mod io; +// pub mod heat; pub mod schedule; +use bevy::prelude::*; + pub trait SolidBody { fn drag_area(&self) -> f32; fn drag_coeff(&self) -> f32; fn total_mass(&self) -> f32; } + +pub struct SimulatorPlugin; + +impl Plugin for SimulatorPlugin { + fn build(&self, app: &mut App) { + // Add systems, resources, and plugins to your app here + } +} diff --git a/src/simulator/schedule.rs b/src/simulator/schedule.rs index 7589aba..42a4795 100644 --- a/src/simulator/schedule.rs +++ b/src/simulator/schedule.rs @@ -1,90 +1,22 @@ -use log::{debug, info, error}; -use std::{ - path::PathBuf, - process::exit, - sync::{ - mpsc::Sender, - Arc, Mutex, - }, - thread::JoinHandle, - time::{Duration, Instant}, -}; +use bevy::prelude::*; -use crate::simulator::{ - balloon::{Balloon, Material}, - bus::{Body, ParachuteSystem}, - config::Config, - forces, - gas::{Atmosphere, GasVolume}, - io::{SimCommands, SimOutput}, - SolidBody, -}; - -/// The simulation state at a given instant -pub struct SimInstant { - pub time: f32, - pub altitude: f32, - pub ascent_rate: f32, - pub acceleration: f32, - pub atmosphere: Atmosphere, - pub balloon: Balloon, - pub body: Body, - pub parachute: ParachuteSystem, +#[derive(Component)] +pub struct EnvConfig { + pub real_time: bool, + pub tick_rate_hz: f32, + pub max_elapsed_time_s: f32, } -fn initialize(config: &Config) -> SimInstant { - // create an initial time step based on the config - let atmo = Atmosphere::new(config.environment.initial_altitude_m); - let material = Material::new(config.balloon.material); - let mut lift_gas = GasVolume::new( - config.balloon.lift_gas.species, - config.balloon.lift_gas.mass_kg, - ); - lift_gas.update_from_ambient(atmo); - let body = Body::new(config.bus.body); - let parachute = - ParachuteSystem::new(config.bus.parachute, 1.0 / config.environment.tick_rate_hz); - - SimInstant { - time: 0.0, - altitude: config.environment.initial_altitude_m, - ascent_rate: config.environment.initial_velocity_m_s, - acceleration: 0.0, - atmosphere: atmo, - balloon: Balloon::new( - material, - config.balloon.thickness_m, - config.balloon.barely_inflated_diameter_m, // ballon diameter (m) - lift_gas, - ), - body, - parachute, - } -} +pub fn step() { -pub fn step(input: SimInstant, config: &Config) -> SimInstant { - // propagate the closed loop simulation forward by one time step - let delta_t = 1.0 / config.environment.tick_rate_hz; - let time = input.time + delta_t; - let mut atmosphere = input.atmosphere; - let mut balloon = input.balloon; - let body = input.body; - let mut parachute = input.parachute; - - if balloon.intact { - // balloon is intact - balloon.stretch(atmosphere.pressure()); - } else { - parachute.deploy(atmosphere, input.ascent_rate); - }; let total_dry_mass = body.total_mass() + parachute.total_mass(); - let weight_force = forces::weight(input.altitude, total_dry_mass); - let buoyancy_force = forces::buoyancy(input.altitude, atmosphere, balloon.lift_gas); + let weight_force = forces::weight(altitude, total_dry_mass); + let buoyancy_force = forces::buoyancy(altitude, atmosphere, balloon.lift_gas); - let total_drag_force = forces::drag(atmosphere, input.ascent_rate, balloon) - + forces::drag(atmosphere, input.ascent_rate, body) - + forces::drag(atmosphere, input.ascent_rate, parachute.main) - + forces::drag(atmosphere, input.ascent_rate, parachute.drogue); + let total_drag_force = forces::drag(atmosphere, ascent_rate, balloon) + + forces::drag(atmosphere, ascent_rate, body) + + forces::drag(atmosphere, ascent_rate, parachute.main) + + forces::drag(atmosphere, ascent_rate, parachute.drogue); debug!( "weight: {:?} buoyancy: {:?} drag: {:?}", weight_force, buoyancy_force, total_drag_force @@ -93,191 +25,8 @@ pub fn step(input: SimInstant, config: &Config) -> SimInstant { // calculate the net force let net_force = weight_force + buoyancy_force + total_drag_force; let acceleration = net_force / total_dry_mass; - let ascent_rate = input.ascent_rate + acceleration * delta_t; - let altitude = input.altitude + ascent_rate * delta_t; + let ascent_rate = ascent_rate + acceleration * delta_t; + let altitude = altitude + ascent_rate * delta_t; atmosphere.set_altitude(altitude); - - SimInstant { - time, - altitude, - ascent_rate, - acceleration, - atmosphere, - balloon, - body, - parachute, - } -} - -pub struct Rate { - cycle_time: Duration, - end_of_last_sleep: Option, -} - -impl Rate { - pub fn new(rate_hz: f32) -> Self { - Self { - cycle_time: Duration::from_secs_f32(1.0 / rate_hz), - end_of_last_sleep: None, - } - } - - pub fn sleep(&mut self) { - let now = Instant::now(); - - let sleep_duration = match self.end_of_last_sleep { - Some(v) => self - .cycle_time - .checked_sub(now.checked_duration_since(v).expect( - "Rate sleep experienced a last sleep with time ahead of the current time", - )) - .expect("Rate sleep detected a blown cycle"), - None => self.cycle_time, - }; - - std::thread::sleep(sleep_duration); - - self.end_of_last_sleep = Some(Instant::now()); - } -} - -pub struct AsyncSim { - config: Config, - sim_output: Arc>, - outpath: PathBuf, - command_sender: Option>, - /// keep track of - run_handle: Option>, -} - -impl AsyncSim { - pub fn new(config: Config, outpath: PathBuf) -> Self { - Self { - config, - sim_output: Arc::new(Mutex::new(SimOutput::default())), - outpath, - command_sender: None, - run_handle: None, - } - } - - pub fn get_sim_output(&self) -> SimOutput { - *self.sim_output.lock().unwrap() - } - - /// Start a thread to run the sim - pub fn start(&mut self) { - if self.run_handle.is_some() { - panic!("Can't start again, sim already ran. Need to stop.") - } - let config = self.config.clone(); - let output = self.sim_output.clone(); - let outpath = self.outpath.clone(); - - debug!("Creating simulation handler..."); - self.run_handle = Some(std::thread::spawn(move || { - debug!("Simulation handler created. Initializing run..."); - AsyncSim::run_sim(config, output, outpath) - })); - } - - pub fn run_sim( - config: Config, - _sim_output: Arc>, - _outpath: PathBuf, - ) { - let mut sim_state = initialize(&config); - // configure simulation - let physics_rate = config.environment.tick_rate_hz; - let max_sim_time = config.environment.max_elapsed_time_s; - let real_time = config.environment.real_time; - let mut rate_sleeper = Rate::new(physics_rate); - - // set up data logger - // let mut writer = init_log_file(outpath); - - info!("Simulation run initialized. Starting loop..."); - loop { - if real_time { - rate_sleeper.sleep(); - } - sim_state = step(sim_state, &config); - - //log output - - // Print log to terminal - debug!( - "[{:.3} s] | Atmosphere @ {:} m: {:} K, {:} Pa", - sim_state.time, - sim_state.altitude, - sim_state.atmosphere.temperature(), - sim_state.atmosphere.temperature() - ); - debug!( - "[{:.3} s] | HAB @ {:.2} m, {:.3} m/s, {:.3} m/s^2 | {:.2} m radius, {:.2} Pa stress, {:.2} % strain", - sim_state.time, - sim_state.altitude, - sim_state.ascent_rate, - sim_state.acceleration, - sim_state.balloon.radius(), - sim_state.balloon.stress(), - sim_state.balloon.strain() * 100.0, - ); - // Stop if there is a problem - if sim_state.altitude.is_nan() - | sim_state.ascent_rate.is_nan() - | sim_state.acceleration.is_nan() - { - let status = format!("Something went wrong, a physical value is NaN!"); - #[cfg(feature = "gui")] - { - error!("{}", status); - break - } - #[cfg(not(feature = "gui"))] - { - Self::terminate(1, status); - } - } - // Run for a certain amount of sim time or to a certain altitude - if sim_state.time >= max_sim_time { - let status = format!("Reached maximum time step ({:?} s)", sim_state.time); - #[cfg(feature = "gui")] - { - info!("{}", status); - break - } - #[cfg(not(feature = "gui"))] - { - Self::terminate( - 0, status, - ); - } - } - if sim_state.altitude < 0.0 { - let status = format!("Altitude at or below zero."); - #[cfg(feature = "gui")] - { - info!("{}", status); - break - } - #[cfg(not(feature = "gui"))] - { - Self::terminate(0, status); - } - } - } - } - fn terminate(code: i32, reason: String) { - if code > 0 { - error!( - "Simulation terminated abnormally with code {:?}. Reason: {:?}", - code, reason - ); - } else { - info!("Simulation terminated normally. Reason: {:?}", reason); - } - exit(code); - } } From 14bd87f698de560b514f44759bc97ee79d868285 Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Thu, 7 Nov 2024 20:21:13 -0500 Subject: [PATCH 04/47] bevy: round 2 --- assets/default.toml | 29 ------------------- assets/setup.ron | 34 ++++++++++++++++++++++ src/config.rs | 55 ----------------------------------- src/main.rs | 1 - src/simulator/balloon.rs | 61 ++++++++++++++++++++++++--------------- src/simulator/bus.rs | 18 ++++++++++++ src/simulator/mod.rs | 25 +++++++++++++++- src/simulator/schedule.rs | 32 -------------------- 8 files changed, 114 insertions(+), 141 deletions(-) delete mode 100644 assets/default.toml create mode 100644 assets/setup.ron delete mode 100644 src/config.rs delete mode 100644 src/simulator/schedule.rs diff --git a/assets/default.toml b/assets/default.toml deleted file mode 100644 index 464dc57..0000000 --- a/assets/default.toml +++ /dev/null @@ -1,29 +0,0 @@ -[environment] -real_time = false -tick_rate_hz = 10.0 -max_elapsed_time_s = 10_000.0 -initial_altitude_m = 0.0 -initial_velocity_m_s = 0.0 - -[balloon] -material = "Rubber" -thickness_m = 0.0001 # thicker balloons have less stress -barely_inflated_diameter_m = 1.5 # larger balloons have less strain - -[balloon.lift_gas] -species = "Helium" -mass_kg = 0.3 - -[bus.body] -mass_kg = 1.427 -drag_area_m2 = 0.04 -drag_coeff = 2.1 - -[bus.parachute] -total_mass_kg = 0.2 -drogue_area_m2 = 0.2 -drogue_drag_coeff = 1.5 -main_area_m2 = 1.1 -main_drag_coeff = 0.8 -deploy_force_n = 100 -deploy_time_s = 5.0 diff --git a/assets/setup.ron b/assets/setup.ron new file mode 100644 index 0000000..a00ad6e --- /dev/null +++ b/assets/setup.ron @@ -0,0 +1,34 @@ +( + environment: ( + real_time: false, + tick_rate_hz: 10.0, + max_elapsed_time_s: 10_000.0, + initial_altitude_m: 0.0, + initial_velocity_m_s: 0.0, + ), + balloon: ( + material: "Rubber", + thickness_m: 0.0001, // thicker balloons have less stress + barely_inflated_diameter_m: 1.5, // larger balloons have less strain + lift_gas: ( + species: "Helium", + mass_kg: 0.3, + ), + ), + bus: ( + body: ( + mass_kg: 1.427, + drag_area_m2: 0.04, + drag_coeff: 2.1, + ), + parachute: ( + total_mass_kg: 0.2, + drogue_area_m2: 0.2, + drogue_drag_coeff: 1.5, + main_area_m2: 1.1, + main_drag_coeff: 0.8, + deploy_force_n: 100, + deploy_time_s: 5.0, + ), + ), +) diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 3489e3f..0000000 --- a/src/config.rs +++ /dev/null @@ -1,55 +0,0 @@ -#[derive(Component)] -pub struct BalloonConfig { - /// Balloon material type - pub material: MaterialType, - /// Thickness of balloon membrane in meters - pub thickness_m: f32, - /// Diameter of "unstressed" balloon membrane when filled, assuming balloon is a sphere, in meters - pub barely_inflated_diameter_m: f32, - /// Configuration for the lift gas - pub lift_gas: GasConfig, -} - -#[derive(Component)] -pub struct GasConfig { - /// Species of the gas - pub species: GasSpecies, - /// Mass of the gas in kilograms - pub mass_kg: f32, -} - -#[derive(Component)] -pub struct BusConfig { - /// Configuration for the body of the bus - pub body: BodyConfig, - /// Configuration for the parachute system - pub parachute: ParachuteConfig, -} - -#[derive(Component)] -pub struct BodyConfig { - /// Mass of all components less ballast material, in kilograms - pub mass_kg: f32, - /// Effective area used for drag calculations during freefall, in square meters - pub drag_area_m2: f32, - /// Drag coefficient of the payload during freefall - pub drag_coeff: f32, -} - -#[derive(Component)] -pub struct ParachuteConfig { - /// Mass of the parachute system (main + drogue), in kilograms - pub total_mass_kg: f32, - /// Drogue parachute effective area used for drag calculations, in square meters - pub drogue_area_m2: f32, - /// Drogue parachute drag coefficient - pub drogue_drag_coeff: f32, - /// Main parachute effective area used for drag calculations, in square meters - pub main_area_m2: f32, - /// Main parachute drag coefficient when fully deployed - pub main_drag_coeff: f32, - /// Force needed for the drogue to deploy the main chute, in Newtons - pub deploy_force_n: f32, - /// Duration the main chute stays in the partially open state, in seconds - pub deploy_time_s: f32, -} diff --git a/src/main.rs b/src/main.rs index 5f4538d..d474b61 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ // mod gui; -mod config; mod simulator; use bevy::prelude::*; diff --git a/src/simulator/balloon.rs b/src/simulator/balloon.rs index d6f7ba2..bc14809 100644 --- a/src/simulator/balloon.rs +++ b/src/simulator/balloon.rs @@ -4,34 +4,46 @@ // Properties, attributes and functions related to the balloon. // ---------------------------------------------------------------------------- - use log::debug; +use ron::de::from_str; use serde::Deserialize; use std::f32::consts::PI; use std::fmt; use std::fs; -use ron::de::from_str; use super::{gas, SolidBody}; +use bevy::prelude::*; #[derive(Clone)] pub struct Balloon<'a> { - pub intact: bool, // whether or not it has burst + pub intact: bool, // whether or not it has burst pub lift_gas: gas::GasVolume<'a>, // gas inside the balloon - pub material: BalloonMaterial, // what the balloon is made of - pub skin_thickness: f32, // thickness of the skin of the balloon (m) - mass: f32, // balloon mass (kg) - drag_coeff: f32, // drag coefficient - unstretched_thickness: f32, // thickness of the skin of the balloon without stretch (m) - unstretched_radius: f32, // radius of balloon without stretch (m) - pub temperature: f32, // fail if surface temperature exceeds this (K) + pub material: BalloonMaterial, // what the balloon is made of + pub skin_thickness: f32, // thickness of the skin of the balloon (m) + mass: f32, // balloon mass (kg) + drag_coeff: f32, // drag coefficient + unstretched_thickness: f32, // thickness of the skin of the balloon without stretch (m) + unstretched_radius: f32, // radius of balloon without stretch (m) + pub temperature: f32, // fail if surface temperature exceeds this (K) stress: f32, strain: f32, } +#[derive(Component)] +pub struct BalloonConfig<'a> { + /// Balloon material type + pub material: BalloonMaterial, + /// Thickness of balloon membrane in meters + pub thickness_m: f32, + /// Diameter of "unstressed" balloon membrane when filled, assuming balloon is a sphere, in meters + pub barely_inflated_diameter_m: f32, + /// Configuration for the lift gas + pub lift_gas: gas::GasVolume<'a>, +} + impl<'a> Balloon<'a> { pub fn new( - material: BalloonMaterial, // material of balloon skin + material: BalloonMaterial, // material of balloon skin skin_thickness: f32, // balloon skin thickness (m) at zero pressure barely_inflated_diameter: f32, // internal diameter (m) at zero pressure lift_gas: gas::GasVolume<'a>, // species of gas inside balloon @@ -93,7 +105,8 @@ impl<'a> Balloon<'a> { // hoop stress (Pa) of thin-walled hollow sphere from internal pressure // https://en.wikipedia.org/wiki/Pressure_vessel#Stress_in_thin-walled_pressure_vessels // https://pkel015.connect.amazon.auckland.ac.nz/SolidMechanicsBooks/Part_I/BookSM_Part_I/07_ElasticityApplications/07_Elasticity_Applications_03_Presure_Vessels.pdf - self.stress = self.gage_pressure(external_pressure) * self.radius() / (2.0 * self.skin_thickness); + self.stress = + self.gage_pressure(external_pressure) * self.radius() / (2.0 * self.skin_thickness); if self.stress > self.material.max_stress { self.burst(format!( "Hoop stress ({:?} Pa) exceeded maximum stress ({:?} Pa)", @@ -172,7 +185,6 @@ impl<'a> Balloon<'a> { self.gage_pressure(external_pressure) ); } - } fn burst(&mut self, reason: String) { @@ -247,7 +259,10 @@ impl BalloonMaterial { pub fn new(material_name: &str) -> Self { let materials = BalloonMaterial::load_materials(); - materials.into_iter().find(|m| m.name == material_name).unwrap_or_default() + materials + .into_iter() + .find(|m| m.name == material_name) + .unwrap_or_default() } } @@ -255,16 +270,16 @@ impl Default for BalloonMaterial { fn default() -> Self { BalloonMaterial { name: "Latex".to_string(), - max_temperature: 373.0, // Example value in Kelvin - density: 920.0, // Example density in kg/m³ - emissivity: 0.9, // Example emissivity - absorptivity: 0.9, // Example absorptivity + max_temperature: 373.0, // Example value in Kelvin + density: 920.0, // Example density in kg/m³ + emissivity: 0.9, // Example emissivity + absorptivity: 0.9, // Example absorptivity thermal_conductivity: 0.13, // Example thermal conductivity in W/mK - specific_heat: 2000.0, // Example specific heat in J/kgK - poissons_ratio: 0.5, // Example Poisson's ratio - elasticity: 0.01e9, // Example Young's Modulus in Pa - max_strain: 0.8, // Example max strain (unitless) - max_stress: 0.5e6, // Example max stress in Pa + specific_heat: 2000.0, // Example specific heat in J/kgK + poissons_ratio: 0.5, // Example Poisson's ratio + elasticity: 0.01e9, // Example Young's Modulus in Pa + max_strain: 0.8, // Example max strain (unitless) + max_stress: 0.5e6, // Example max stress in Pa } } } diff --git a/src/simulator/bus.rs b/src/simulator/bus.rs index 1e96d9b..db6f98d 100644 --- a/src/simulator/bus.rs +++ b/src/simulator/bus.rs @@ -137,3 +137,21 @@ impl ParachuteSystem { self.main.deploy(drogue_drag); } } + + +pub struct ParachuteConfig { + /// Mass of the parachute system (main + drogue), in kilograms + pub total_mass_kg: f32, + /// Drogue parachute effective area used for drag calculations, in square meters + pub drogue_area_m2: f32, + /// Drogue parachute drag coefficient + pub drogue_drag_coeff: f32, + /// Main parachute effective area used for drag calculations, in square meters + pub main_area_m2: f32, + /// Main parachute drag coefficient when fully deployed + pub main_drag_coeff: f32, + /// Force needed for the drogue to deploy the main chute, in Newtons + pub deploy_force_n: f32, + /// Duration the main chute stays in the partially open state, in seconds + pub deploy_time_s: f32, +} diff --git a/src/simulator/mod.rs b/src/simulator/mod.rs index 5a78c37..b5782e9 100644 --- a/src/simulator/mod.rs +++ b/src/simulator/mod.rs @@ -4,7 +4,6 @@ pub mod constants; pub mod forces; pub mod gas; // pub mod heat; -pub mod schedule; use bevy::prelude::*; @@ -21,3 +20,27 @@ impl Plugin for SimulatorPlugin { // Add systems, resources, and plugins to your app here } } + +// pub fn step() { + +// let total_dry_mass = body.total_mass() + parachute.total_mass(); +// let weight_force = forces::weight(altitude, total_dry_mass); +// let buoyancy_force = forces::buoyancy(altitude, atmosphere, balloon.lift_gas); + +// let total_drag_force = forces::drag(atmosphere, ascent_rate, balloon) +// + forces::drag(atmosphere, ascent_rate, body) +// + forces::drag(atmosphere, ascent_rate, parachute.main) +// + forces::drag(atmosphere, ascent_rate, parachute.drogue); +// debug!( +// "weight: {:?} buoyancy: {:?} drag: {:?}", +// weight_force, buoyancy_force, total_drag_force +// ); + +// // calculate the net force +// let net_force = weight_force + buoyancy_force + total_drag_force; +// let acceleration = net_force / total_dry_mass; +// let ascent_rate = ascent_rate + acceleration * delta_t; +// let altitude = altitude + ascent_rate * delta_t; + +// atmosphere.set_altitude(altitude); +// } diff --git a/src/simulator/schedule.rs b/src/simulator/schedule.rs deleted file mode 100644 index 42a4795..0000000 --- a/src/simulator/schedule.rs +++ /dev/null @@ -1,32 +0,0 @@ -use bevy::prelude::*; - -#[derive(Component)] -pub struct EnvConfig { - pub real_time: bool, - pub tick_rate_hz: f32, - pub max_elapsed_time_s: f32, -} - -pub fn step() { - - let total_dry_mass = body.total_mass() + parachute.total_mass(); - let weight_force = forces::weight(altitude, total_dry_mass); - let buoyancy_force = forces::buoyancy(altitude, atmosphere, balloon.lift_gas); - - let total_drag_force = forces::drag(atmosphere, ascent_rate, balloon) - + forces::drag(atmosphere, ascent_rate, body) - + forces::drag(atmosphere, ascent_rate, parachute.main) - + forces::drag(atmosphere, ascent_rate, parachute.drogue); - debug!( - "weight: {:?} buoyancy: {:?} drag: {:?}", - weight_force, buoyancy_force, total_drag_force - ); - - // calculate the net force - let net_force = weight_force + buoyancy_force + total_drag_force; - let acceleration = net_force / total_dry_mass; - let ascent_rate = ascent_rate + acceleration * delta_t; - let altitude = altitude + ascent_rate * delta_t; - - atmosphere.set_altitude(altitude); -} From ef58ca7856b74d8d4d4f1f7e1e232084254e15e0 Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Thu, 7 Nov 2024 20:35:49 -0500 Subject: [PATCH 05/47] bevy: round 3 --- src/main.rs | 3 +- src/simulator/atmosphere.rs | 116 +++++++++++++++++++++++++++++ src/simulator/balloon.rs | 8 ++ src/simulator/forces.rs | 10 +-- src/simulator/gas.rs | 143 ++++-------------------------------- src/simulator/mod.rs | 12 ++- src/simulator/units.rs | 4 + src/{gui => ui}/mod.rs | 0 src/{gui => ui}/shell.rs | 0 src/{gui => ui}/views.rs | 0 10 files changed, 156 insertions(+), 140 deletions(-) create mode 100644 src/simulator/atmosphere.rs create mode 100644 src/simulator/units.rs rename src/{gui => ui}/mod.rs (100%) rename src/{gui => ui}/shell.rs (100%) rename src/{gui => ui}/views.rs (100%) diff --git a/src/main.rs b/src/main.rs index d474b61..7329907 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,8 @@ fn main() { }), ..default() }), - // gui::InterfacePlugins, + // ui::InterfacePlugins, + simulator::SimulatorPlugins, )) .run(); } diff --git a/src/simulator/atmosphere.rs b/src/simulator/atmosphere.rs new file mode 100644 index 0000000..965cce6 --- /dev/null +++ b/src/simulator/atmosphere.rs @@ -0,0 +1,116 @@ +use bevy::prelude::*; +use std::fmt; + +use crate::simulator::{ + constants::*, + gas::density, + units::celsius2kelvin, +}; + +#[derive(Copy, Clone)] +pub struct Atmosphere { + // US Standard Atmosphere, 1976 + altitude: f32, // [m] altitude (which determines the other attributes) + temperature: f32, // [K] temperature + pressure: f32, // [Pa] pressure + density: f32, // [kg/m³] density + molar_mass: f32, // [kg/mol] molar mass a.k.a. molecular weight +} + +impl fmt::Display for Atmosphere { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{:} K | {:} Pa | {:} kg/m³", + self.temperature, self.pressure, self.density, + ) + } +} + +impl Atmosphere { + pub fn new(altitude: f32) -> Self { + Atmosphere { + altitude, + temperature: coesa_temperature(altitude), + pressure: coesa_pressure(altitude), + density: density( + coesa_temperature(altitude), + coesa_pressure(altitude), + 28.9647, + ), + molar_mass: 28.9647, + } + } + pub fn set_altitude(&mut self, new_altitude: f32) { + self.altitude = new_altitude; + // update all params + self.temperature = coesa_temperature(new_altitude); + self.pressure = coesa_pressure(new_altitude); + self.density = density(self.temperature, self.pressure, self.molar_mass); + } + + pub fn temperature(self) -> f32 { + // Temperature (K) + self.temperature + } + + pub fn pressure(self) -> f32 { + // Pressure (Pa) + self.pressure + } + + pub fn density(self) -> f32 { + // Density (kg/m³) + self.density + } +} + +impl Default for Atmosphere { + fn default() -> Self { + Atmosphere { + altitude: 0.0, // Sea level + temperature: STANDARD_TEMPERATURE, // Standard sea level temperature + pressure: STANDARD_PRESSURE, // Standard sea level pressure + density: density(STANDARD_TEMPERATURE, STANDARD_PRESSURE, 28.9647), // Calculate density at sea level + molar_mass: 28.9647, // Molar mass of air + } + } +} + +fn coesa_temperature(altitude: f32) -> f32 { + // 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) + if (-57.0..11000.0).contains(&altitude) { + celsius2kelvin(15.04 - 0.00649 * altitude) + } else if (11000.0..25000.0).contains(&altitude) { + celsius2kelvin(-56.46) + } else if (25000.0..85000.0).contains(&altitude) { + celsius2kelvin(-131.21 + 0.00299 * altitude) + } else { + error!( + "Altitude {:}m is outside of the accepted range! Must be 0-85000m", + altitude + ); + 0.0 + } +} + +fn coesa_pressure(altitude: f32) -> f32 { + // 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) + if (-57.0..11000.0).contains(&altitude) { + (101.29 * f32::powf(coesa_temperature(altitude) / 288.08, 5.256)) * 1000.0 + } else if (11000.0..25000.0).contains(&altitude) { + (22.65 * f32::exp(1.73 - 0.000157 * altitude)) * 1000.0 + } else if (25000.0..85000.0).contains(&altitude) { + (2.488 * f32::powf(coesa_temperature(altitude) / 216.6, -11.388)) * 1000.0 + } else { + error!( + "Altitude {:}m is outside of the accepted range! Must be 0-85000m", + altitude + ); + 0.0 + } +} diff --git a/src/simulator/balloon.rs b/src/simulator/balloon.rs index bc14809..525e947 100644 --- a/src/simulator/balloon.rs +++ b/src/simulator/balloon.rs @@ -14,6 +14,14 @@ use std::fs; use super::{gas, SolidBody}; use bevy::prelude::*; +pub struct BalloonPlugin; + +impl Plugin for BalloonPlugin { + fn build(&self, app: &mut App) { + // app.add_systems(Update, step); + } +} + #[derive(Clone)] pub struct Balloon<'a> { pub intact: bool, // whether or not it has burst diff --git a/src/simulator/forces.rs b/src/simulator/forces.rs index effb24d..9aaa2c2 100644 --- a/src/simulator/forces.rs +++ b/src/simulator/forces.rs @@ -6,7 +6,7 @@ // ---------------------------------------------------------------------------- use super::constants::{EARTH_RADIUS_M, STANDARD_G}; -use super::{gas, SolidBody}; +use super::{gas, atmosphere, SolidBody}; fn g(altitude: f32) -> f32 { // Acceleration (m/s^2) from gravity at an altitude (m) above mean sea level. @@ -18,7 +18,7 @@ pub fn weight(altitude: f32, mass: f32) -> f32 { g(altitude) * mass // [N] } -pub fn buoyancy(altitude: f32, atmo: gas::Atmosphere, lift_gas: gas::GasVolume) -> f32 { +pub fn buoyancy(altitude: f32, atmo: atmosphere::Atmosphere, lift_gas: gas::GasVolume) -> f32 { // Force (N) due to air displaced by the given gas volume. let v = lift_gas.volume(); if v > 0.0 { @@ -30,20 +30,20 @@ pub fn buoyancy(altitude: f32, atmo: gas::Atmosphere, lift_gas: gas::GasVolume) } } -pub fn drag(atmo: gas::Atmosphere, velocity: f32, body: T) -> f32 { +pub fn drag(atmo: atmosphere::Atmosphere, velocity: f32, body: T) -> f32 { // Force (N) due to drag against the balloon let direction = -f32::copysign(1.0, velocity); direction * body.drag_coeff() / 2.0 * atmo.density() * f32::powf(velocity, 2.0) * body.drag_area() } -pub fn gross_lift(atmo: gas::Atmosphere, lift_gas: gas::GasVolume) -> f32 { +pub fn gross_lift(atmo: atmosphere::Atmosphere, lift_gas: gas::GasVolume) -> f32 { // [kg] let rho_atmo = atmo.density(); let rho_lift = lift_gas.density(); lift_gas.volume() * (rho_lift - rho_atmo) } -pub fn free_lift(atmo: gas::Atmosphere, lift_gas: gas::GasVolume, total_dry_mass: f32) -> f32 { +pub fn free_lift(atmo: atmosphere::Atmosphere, lift_gas: gas::GasVolume, total_dry_mass: f32) -> f32 { // [kg] gross_lift(atmo, lift_gas) - total_dry_mass } diff --git a/src/simulator/gas.rs b/src/simulator/gas.rs index b94fac4..3fb4a3a 100644 --- a/src/simulator/gas.rs +++ b/src/simulator/gas.rs @@ -25,6 +25,19 @@ use serde::Deserialize; use std::fmt; use std::fs; +pub fn volume(temperature: f32, pressure: f32, mass: f32, molar_mass: f32) -> f32 { + // Volume (m³) of an ideal gas from its temperature (K), pressure (Pa), + // mass (kg) and molar mass (kg/mol). + (mass / molar_mass) * R * temperature / pressure // [m³] +} + +pub fn density(temperature: f32, pressure: f32, molar_mass: f32) -> f32 { + // Density (kg/m³) of an ideal gas frorm its temperature (K), pressure (Pa), + // and molar mass (kg/mol) + (molar_mass * pressure) / (R * temperature) // [kg/m³] +} + + #[derive(Debug, Deserialize, Clone)] pub struct GasSpecies { pub name: String, @@ -81,11 +94,6 @@ impl<'a> GasVolume<'a> { } } - pub fn update_from_ambient(&mut self, atmo: Atmosphere) { - self.temperature = atmo.temperature(); - self.pressure = atmo.pressure(); - } - pub fn set_temperature(&mut self, new_temperature: f32) { // set the temperature (K) of the GasVolume self.temperature = new_temperature; @@ -145,131 +153,6 @@ impl<'a> GasVolume<'a> { } } -#[derive(Copy, Clone)] -pub struct Atmosphere { - // US Standard Atmosphere, 1976 - altitude: f32, // [m] altitude (which determines the other attributes) - temperature: f32, // [K] temperature - pressure: f32, // [Pa] pressure - density: f32, // [kg/m³] density - molar_mass: f32, // [kg/mol] molar mass a.k.a. molecular weight -} - -impl fmt::Display for Atmosphere { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{:} K | {:} Pa | {:} kg/m³", - self.temperature, self.pressure, self.density, - ) - } -} - -impl Atmosphere { - pub fn new(altitude: f32) -> Self { - Atmosphere { - altitude, - temperature: coesa_temperature(altitude), - pressure: coesa_pressure(altitude), - density: density( - coesa_temperature(altitude), - coesa_pressure(altitude), - 28.9647, - ), - molar_mass: 28.9647, - } - } - pub fn set_altitude(&mut self, new_altitude: f32) { - self.altitude = new_altitude; - // update all params - self.temperature = coesa_temperature(new_altitude); - self.pressure = coesa_pressure(new_altitude); - self.density = density(self.temperature, self.pressure, self.molar_mass); - } - - pub fn temperature(self) -> f32 { - // Temperature (K) - self.temperature - } - - pub fn pressure(self) -> f32 { - // Pressure (Pa) - self.pressure - } - - pub fn density(self) -> f32 { - // Density (kg/m³) - self.density - } -} - -impl Default for Atmosphere { - fn default() -> Self { - Atmosphere { - altitude: 0.0, // Sea level - temperature: STANDARD_TEMPERATURE, // Standard sea level temperature - pressure: STANDARD_PRESSURE, // Standard sea level pressure - density: density(STANDARD_TEMPERATURE, STANDARD_PRESSURE, 28.9647), // Calculate density at sea level - molar_mass: 28.9647, // Molar mass of air - } - } -} - -fn volume(temperature: f32, pressure: f32, mass: f32, molar_mass: f32) -> f32 { - // Volume (m³) of an ideal gas from its temperature (K), pressure (Pa), - // mass (kg) and molar mass (kg/mol). - (mass / molar_mass) * R * temperature / pressure // [m³] -} - -fn density(temperature: f32, pressure: f32, molar_mass: f32) -> f32 { - // Density (kg/m³) of an ideal gas frorm its temperature (K), pressure (Pa), - // and molar mass (kg/mol) - (molar_mass * pressure) / (R * temperature) // [kg/m³] -} - -fn coesa_temperature(altitude: f32) -> f32 { - // 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) - if (-57.0..11000.0).contains(&altitude) { - celsius2kelvin(15.04 - 0.00649 * altitude) - } else if (11000.0..25000.0).contains(&altitude) { - celsius2kelvin(-56.46) - } else if (25000.0..85000.0).contains(&altitude) { - celsius2kelvin(-131.21 + 0.00299 * altitude) - } else { - error!( - "Altitude {:}m is outside of the accepted range! Must be 0-85000m", - altitude - ); - 0.0 - } -} - -fn coesa_pressure(altitude: f32) -> f32 { - // 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) - if (-57.0..11000.0).contains(&altitude) { - (101.29 * f32::powf(coesa_temperature(altitude) / 288.08, 5.256)) * 1000.0 - } else if (11000.0..25000.0).contains(&altitude) { - (22.65 * f32::exp(1.73 - 0.000157 * altitude)) * 1000.0 - } else if (25000.0..85000.0).contains(&altitude) { - (2.488 * f32::powf(coesa_temperature(altitude) / 216.6, -11.388)) * 1000.0 - } else { - error!( - "Altitude {:}m is outside of the accepted range! Must be 0-85000m", - altitude - ); - 0.0 - } -} - -fn celsius2kelvin(deg_celsius: f32) -> f32 { - // Convert degrees C to Kelvin - deg_celsius + 273.15 -} - #[derive(Debug, Deserialize)] struct GasConfig { gases: Vec, diff --git a/src/simulator/mod.rs b/src/simulator/mod.rs index b5782e9..3ad8e01 100644 --- a/src/simulator/mod.rs +++ b/src/simulator/mod.rs @@ -3,9 +3,12 @@ pub mod balloon; pub mod constants; pub mod forces; pub mod gas; +pub mod atmosphere; +pub mod units; // pub mod heat; use bevy::prelude::*; +use bevy::app::PluginGroupBuilder; pub trait SolidBody { fn drag_area(&self) -> f32; @@ -13,11 +16,12 @@ pub trait SolidBody { fn total_mass(&self) -> f32; } -pub struct SimulatorPlugin; +pub struct SimulatorPlugins; -impl Plugin for SimulatorPlugin { - fn build(&self, app: &mut App) { - // Add systems, resources, and plugins to your app here +impl PluginGroup for SimulatorPlugins { + fn build(self) -> PluginGroupBuilder { + PluginGroupBuilder::start::() + .add(balloon::BalloonPlugin) } } diff --git a/src/simulator/units.rs b/src/simulator/units.rs new file mode 100644 index 0000000..2258cd0 --- /dev/null +++ b/src/simulator/units.rs @@ -0,0 +1,4 @@ +pub fn celsius2kelvin(deg_celsius: f32) -> f32 { + // Convert degrees C to Kelvin + deg_celsius + 273.15 +} diff --git a/src/gui/mod.rs b/src/ui/mod.rs similarity index 100% rename from src/gui/mod.rs rename to src/ui/mod.rs diff --git a/src/gui/shell.rs b/src/ui/shell.rs similarity index 100% rename from src/gui/shell.rs rename to src/ui/shell.rs diff --git a/src/gui/views.rs b/src/ui/views.rs similarity index 100% rename from src/gui/views.rs rename to src/ui/views.rs From ad84a63eeb5f1672cf77f94a933a8bc87d3936fd Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Thu, 7 Nov 2024 20:41:34 -0500 Subject: [PATCH 06/47] docs: devlog for 2024-11-07 --- docs/devlog.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/devlog.md b/docs/devlog.md index 004f4f1..931e6c5 100644 --- a/docs/devlog.md +++ b/docs/devlog.md @@ -35,3 +35,33 @@ I had in the previous Rust simulation. Namely, I need to: The Bevy schedule system will completely replace the threaded event loop that I had in the previous simulation, including things like `SimCommands`, `SimOutput`, `SimInstant`, and the `AsyncSim` struct. + +### Changelog - 2024-11-07 + +- **Main Application (`src/main.rs`):** + - Integrated `simulator::SimulatorPlugins` to replace the previous UI plugins + setup. + +- **Atmosphere Module (`src/simulator/atmosphere.rs`):** + - Introduced a new `Atmosphere` struct to model atmospheric conditions using + the US Standard Atmosphere, 1976. + - Added functions for calculating temperature and pressure based on altitude. + +- **Balloon Module (`src/simulator/balloon.rs`):** + - Added `BalloonPlugin` struct implementing the `Plugin` trait for Bevy + integration. + +- **Forces Module (`src/simulator/forces.rs`):** + - Updated to use the new `atmosphere::Atmosphere` for buoyancy and drag + calculations. + +- **Gas Module (`src/simulator/gas.rs`):** + - Moved `Atmosphere` struct to its own module. + - Added utility functions for calculating gas volume and density. + +- **Simulator Module (`src/simulator/mod.rs`):** + - Added new modules: `atmosphere` and `units`. + - Converted `SimulatorPlugin` to `SimulatorPlugins` as a `PluginGroup`. + +- **Units Module (`src/simulator/units.rs`):** + - Added a utility function to convert Celsius to Kelvin. From 7e2d6cc28483bb03af75d6c2500508fa9087f916 Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Fri, 8 Nov 2024 00:44:13 -0500 Subject: [PATCH 07/47] bevy: round 4 --- Cargo.toml | 2 +- assets/materials.ron | 43 ----- assets/{gases.ron => properties.ron} | 41 ++++ src/assets.rs | 46 +++++ src/main.rs | 15 +- src/simulator/atmosphere.rs | 81 ++------ src/simulator/balloon.rs | 62 ++---- src/simulator/constants.rs | 1 + src/simulator/forces.rs | 15 +- src/simulator/gas.rs | 15 +- src/simulator/mod.rs | 3 +- src/ui/balloon_designer.rs | 67 +++++++ src/ui/mod.rs | 42 +--- src/ui/shell.rs | 279 +++------------------------ src/ui/views.rs | 2 +- 15 files changed, 249 insertions(+), 465 deletions(-) delete mode 100644 assets/materials.ron rename assets/{gases.ron => properties.ron} (53%) create mode 100644 src/assets.rs create mode 100644 src/ui/balloon_designer.rs diff --git a/Cargo.toml b/Cargo.toml index aee697e..7da7bdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,13 @@ license-file = "LICENSE" [dependencies] pretty_env_logger = "0.5.0" serde = { version = "1.0.214", features = ["derive"] } -log = { version = "0.4.20", features = ["release_max_level_debug"] } egui_plot = { version = "0.29.0", features = ["serde"] } egui_extras = { version = "0.29.1", features = ["file"] } bevy = "0.14.2" bevy_egui = "0.30.0" avian3d = "0.1.2" ron = "0.8.1" +bevy_common_assets = { version = "0.11.0", features = ["ron"] } [[bin]] name = "yahs" diff --git a/assets/materials.ron b/assets/materials.ron deleted file mode 100644 index eedb905..0000000 --- a/assets/materials.ron +++ /dev/null @@ -1,43 +0,0 @@ -( - materials: [ - ( - name: "Nothing", - max_temperature: Infinity, - density: 0.0, - emissivity: 1.0, - absorptivity: 0.0, - thermal_conductivity: Infinity, - specific_heat: 0.0, - poissons_ratio: 0.5, - elasticity: Infinity, - max_strain: Infinity, - max_stress: Infinity, - ), - ( - name: "Rubber", - max_temperature: 385.0, - density: 1000.0, - emissivity: 0.86, - absorptivity: 0.86, - thermal_conductivity: 0.25, - specific_heat: 1490.0, - poissons_ratio: 0.5, - elasticity: 4_000_000.0, - max_strain: 8.0, - max_stress: 25_000_000.0, - ), - ( - name: "LowDensityPolyethylene", - max_temperature: 348.0, - density: 919.0, - emissivity: 0.94, - absorptivity: 0.94, - thermal_conductivity: 0.3175, - specific_heat: 2600.0, - poissons_ratio: 0.5, - elasticity: 300_000_000.0, - max_strain: 6.25, - max_stress: 10_000_000.0, - ), - ] -) diff --git a/assets/gases.ron b/assets/properties.ron similarity index 53% rename from assets/gases.ron rename to assets/properties.ron index 4e3d83d..58b0b48 100644 --- a/assets/gases.ron +++ b/assets/properties.ron @@ -56,5 +56,46 @@ abbreviation: "CH4", molar_mass: 0.01604303, ), + ], + materials: [ + ( + name: "Nothing", + max_temperature: 1e30, + density: 0.0, + emissivity: 1.0, + absorptivity: 0.0, + thermal_conductivity: 1e30, + specific_heat: 0.0, + poissons_ratio: 0.5, + elasticity: 1e30, + max_strain: 1e30, + max_stress: 1e30, + ), + ( + name: "Rubber", + max_temperature: 385.0, + density: 1000.0, + emissivity: 0.86, + absorptivity: 0.86, + thermal_conductivity: 0.25, + specific_heat: 1490.0, + poissons_ratio: 0.5, + elasticity: 4000000.0, + max_strain: 8.0, + max_stress: 25000000.0, + ), + ( + name: "LowDensityPolyethylene", + max_temperature: 348.0, + density: 919.0, + emissivity: 0.94, + absorptivity: 0.94, + thermal_conductivity: 0.3175, + specific_heat: 2600.0, + poissons_ratio: 0.5, + elasticity: 300_000_000.0, + max_strain: 6.25, + max_stress: 10_000_000.0, + ), ] ) diff --git a/src/assets.rs b/src/assets.rs new file mode 100644 index 0000000..d763fcb --- /dev/null +++ b/src/assets.rs @@ -0,0 +1,46 @@ +use bevy::prelude::*; +use bevy_common_assets::ron::RonAssetPlugin; +use serde::Deserialize; + +use crate::simulator::{balloon::BalloonMaterial, gas::GasSpecies}; +use crate::AppState; + +#[derive(Resource, Asset, TypePath, Debug, Deserialize)] +pub struct PropertiesConfig { + pub gases: Vec, + pub materials: Vec, +} + +#[derive(Resource)] +pub struct PropertiesConfigHandle(Handle); + +pub struct AssetLoaderPlugin; + +impl Plugin for AssetLoaderPlugin { + fn build(&self, app: &mut App) { + app.add_plugins((RonAssetPlugin::::new(&["ron"]),)) + .add_systems(Startup, setup_asset_loader) + .add_systems(OnEnter(AppState::Loading), load_assets); + } +} + +fn setup_asset_loader(asset_server: Res, mut commands: Commands) { + commands.insert_resource(PropertiesConfigHandle(asset_server.load("properties.ron"))); +} + +fn load_assets( + properties_handle: Res, + properties: Res>, + mut commands: Commands, + mut state: ResMut>, +) { + if let Some(properties_config) = properties.get(&properties_handle.0) { + // Insert the loaded properties as a resource + commands.insert_resource(PropertiesConfig { + gases: properties_config.gases.clone(), + materials: properties_config.materials.clone(), + }); + // Transition to the Running state + state.set(AppState::Running); + } +} diff --git a/src/main.rs b/src/main.rs index 7329907..bb1f5ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,19 @@ -// mod gui; +mod ui; mod simulator; - +mod assets; use bevy::prelude::*; +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)] +enum AppState { + #[default] + Loading, + Running, +} + fn main() { setup_pretty_logs(); App::new() + .init_state::() .add_plugins(( DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { @@ -14,8 +22,9 @@ fn main() { }), ..default() }), - // ui::InterfacePlugins, + ui::InterfacePlugins, simulator::SimulatorPlugins, + assets::AssetLoaderPlugin, )) .run(); } diff --git a/src/simulator/atmosphere.rs b/src/simulator/atmosphere.rs index 965cce6..2971b3e 100644 --- a/src/simulator/atmosphere.rs +++ b/src/simulator/atmosphere.rs @@ -1,79 +1,40 @@ use bevy::prelude::*; -use std::fmt; use crate::simulator::{ - constants::*, + constants::ATMOSPHERE_MOLAR_MASS, gas::density, units::celsius2kelvin, }; -#[derive(Copy, Clone)] -pub struct Atmosphere { - // US Standard Atmosphere, 1976 - altitude: f32, // [m] altitude (which determines the other attributes) - temperature: f32, // [K] temperature - pressure: f32, // [Pa] pressure - density: f32, // [kg/m³] density - molar_mass: f32, // [kg/mol] molar mass a.k.a. molecular weight -} - -impl fmt::Display for Atmosphere { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{:} K | {:} Pa | {:} kg/m³", - self.temperature, self.pressure, self.density, - ) +pub struct AtmospherePlugin; +impl Plugin for AtmospherePlugin { + fn build(&self, app: &mut App) { + app.insert_resource(Atmosphere); } } -impl Atmosphere { - pub fn new(altitude: f32) -> Self { - Atmosphere { - altitude, - temperature: coesa_temperature(altitude), - pressure: coesa_pressure(altitude), - density: density( - coesa_temperature(altitude), - coesa_pressure(altitude), - 28.9647, - ), - molar_mass: 28.9647, - } - } - pub fn set_altitude(&mut self, new_altitude: f32) { - self.altitude = new_altitude; - // update all params - self.temperature = coesa_temperature(new_altitude); - self.pressure = coesa_pressure(new_altitude); - self.density = density(self.temperature, self.pressure, self.molar_mass); - } +/// US Standard Atmosphere, 1976 +#[derive(Resource)] +pub struct Atmosphere; - pub fn temperature(self) -> f32 { +impl Atmosphere { + pub fn temperature(&self, altitude: f32) -> f32 { // Temperature (K) - self.temperature + coesa_temperature(altitude) } - - pub fn pressure(self) -> f32 { + + pub fn pressure(&self, altitude: f32) -> f32 { // Pressure (Pa) - self.pressure - } - - pub fn density(self) -> f32 { - // Density (kg/m³) - self.density + coesa_pressure(altitude) } -} -impl Default for Atmosphere { - fn default() -> Self { - Atmosphere { - altitude: 0.0, // Sea level - temperature: STANDARD_TEMPERATURE, // Standard sea level temperature - pressure: STANDARD_PRESSURE, // Standard sea level pressure - density: density(STANDARD_TEMPERATURE, STANDARD_PRESSURE, 28.9647), // Calculate density at sea level - molar_mass: 28.9647, // Molar mass of air - } + pub fn density(&self, altitude: f32) -> f32 { + // Ensure the function signature matches the expected parameters + density( + coesa_temperature(altitude), + coesa_pressure(altitude), + ATMOSPHERE_MOLAR_MASS, + ) } } diff --git a/src/simulator/balloon.rs b/src/simulator/balloon.rs index 525e947..5859f39 100644 --- a/src/simulator/balloon.rs +++ b/src/simulator/balloon.rs @@ -4,12 +4,9 @@ // Properties, attributes and functions related to the balloon. // ---------------------------------------------------------------------------- -use log::debug; -use ron::de::from_str; +use bevy::log::debug; use serde::Deserialize; -use std::f32::consts::PI; -use std::fmt; -use std::fs; +use std::{f32::consts::PI, fmt}; use super::{gas, SolidBody}; use bevy::prelude::*; @@ -200,7 +197,7 @@ impl<'a> Balloon<'a> { self.intact = false; self.set_volume(0.0); self.lift_gas.set_mass(0.0); - log::warn!("The balloon has burst! Reason: {:?}", reason) + bevy::log::warn!("The balloon has burst! Reason: {:?}", reason) } } @@ -253,24 +250,24 @@ pub fn projected_spherical_area(volume: f32) -> f32 { // Source: https://www.matweb.com/ // ---------------------------------------------------------------------------- -#[derive(Debug, Deserialize, Clone)] -pub struct MaterialConfig { - materials: Vec, +#[derive(Debug, Deserialize, Clone, PartialEq)] +pub struct BalloonMaterial { + pub name: String, + pub max_temperature: f32, // temperature (K) where the given material fails + pub density: f32, // density (kg/m³) + pub emissivity: f32, // how much thermal radiation is emitted + pub absorptivity: f32, // how much thermal radiation is absorbed + pub thermal_conductivity: f32, // thermal conductivity (W/mK) of the material at room temperature + pub specific_heat: f32, // J/kgK + pub poissons_ratio: f32, // ratio of change in width for a given change in length + pub elasticity: f32, // Youngs Modulus aka Modulus of Elasticity (Pa) + pub max_strain: f32, // elongation at failure (decimal, unitless) 1 = original size + pub max_stress: f32, // tangential stress at failure (Pa) } -impl BalloonMaterial { - pub fn load_materials() -> Vec { - let content = fs::read_to_string("path/to/materials.ron").expect("Unable to read file"); - let config: MaterialConfig = from_str(&content).expect("Unable to parse RON"); - config.materials - } - - pub fn new(material_name: &str) -> Self { - let materials = BalloonMaterial::load_materials(); - materials - .into_iter() - .find(|m| m.name == material_name) - .unwrap_or_default() +impl fmt::Display for BalloonMaterial { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name) } } @@ -291,24 +288,3 @@ impl Default for BalloonMaterial { } } } - -#[derive(Debug, Deserialize, Clone, PartialEq)] -pub struct BalloonMaterial { - pub name: String, - pub max_temperature: f32, // temperature (K) where the given material fails - pub density: f32, // density (kg/m³) - pub emissivity: f32, // how much thermal radiation is emitted - pub absorptivity: f32, // how much thermal radiation is absorbed - pub thermal_conductivity: f32, // thermal conductivity (W/mK) of the material at room temperature - pub specific_heat: f32, // J/kgK - pub poissons_ratio: f32, // ratio of change in width for a given change in length - pub elasticity: f32, // Youngs Modulus aka Modulus of Elasticity (Pa) - pub max_strain: f32, // elongation at failure (decimal, unitless) 1 = original size - pub max_stress: f32, // tangential stress at failure (Pa) -} - -impl fmt::Display for BalloonMaterial { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.name) - } -} diff --git a/src/simulator/constants.rs b/src/simulator/constants.rs index 1138fdc..6710eb1 100644 --- a/src/simulator/constants.rs +++ b/src/simulator/constants.rs @@ -12,3 +12,4 @@ pub const R: f32 = BOLTZMANN_CONSTANT * AVOGADRO_CONSTANT; // [J/K-mol] Ideal ga pub const STANDARD_G: f32 = 9.80665; // [m/s^2] standard gravitational acceleration pub const EARTH_RADIUS_M: f32 = 6371007.2; // [m] mean radius of Earth +pub const ATMOSPHERE_MOLAR_MASS: f32 = 0.02897; // [kg/mol] molar mass of air diff --git a/src/simulator/forces.rs b/src/simulator/forces.rs index 9aaa2c2..b0ef268 100644 --- a/src/simulator/forces.rs +++ b/src/simulator/forces.rs @@ -4,6 +4,7 @@ // Forces that act in the vertical axis. All forces assume a positive-up // coordinate frame and aR_E R_Eported in Newtons. // ---------------------------------------------------------------------------- +#[allow(dead_code)] use super::constants::{EARTH_RADIUS_M, STANDARD_G}; use super::{gas, atmosphere, SolidBody}; @@ -22,7 +23,7 @@ pub fn buoyancy(altitude: f32, atmo: atmosphere::Atmosphere, lift_gas: gas::GasV // Force (N) due to air displaced by the given gas volume. let v = lift_gas.volume(); if v > 0.0 { - let rho_atmo = atmo.density(); + let rho_atmo = atmo.density(altitude); let rho_lift = lift_gas.density(); return lift_gas.volume() * (rho_lift - rho_atmo) * g(altitude) } else { @@ -30,20 +31,20 @@ pub fn buoyancy(altitude: f32, atmo: atmosphere::Atmosphere, lift_gas: gas::GasV } } -pub fn drag(atmo: atmosphere::Atmosphere, velocity: f32, body: T) -> f32 { +pub fn drag(altitude: f32, atmo: atmosphere::Atmosphere, velocity: f32, body: T) -> f32 { // Force (N) due to drag against the balloon let direction = -f32::copysign(1.0, velocity); - direction * body.drag_coeff() / 2.0 * atmo.density() * f32::powf(velocity, 2.0) * body.drag_area() + direction * body.drag_coeff() / 2.0 * atmo.density(altitude) * f32::powf(velocity, 2.0) * body.drag_area() } -pub fn gross_lift(atmo: atmosphere::Atmosphere, lift_gas: gas::GasVolume) -> f32 { +pub fn gross_lift(altitude: f32, atmo: atmosphere::Atmosphere, lift_gas: gas::GasVolume) -> f32 { // [kg] - let rho_atmo = atmo.density(); + let rho_atmo = atmo.density(altitude); let rho_lift = lift_gas.density(); lift_gas.volume() * (rho_lift - rho_atmo) } -pub fn free_lift(atmo: atmosphere::Atmosphere, lift_gas: gas::GasVolume, total_dry_mass: f32) -> f32 { +pub fn free_lift(altitude: f32, atmo: atmosphere::Atmosphere, lift_gas: gas::GasVolume, total_dry_mass: f32) -> f32 { // [kg] - gross_lift(atmo, lift_gas) - total_dry_mass + gross_lift(altitude, atmo, lift_gas) - total_dry_mass } diff --git a/src/simulator/gas.rs b/src/simulator/gas.rs index 3fb4a3a..9066897 100644 --- a/src/simulator/gas.rs +++ b/src/simulator/gas.rs @@ -19,11 +19,9 @@ #![allow(dead_code)] use super::constants::{R, STANDARD_PRESSURE, STANDARD_TEMPERATURE}; -use log::error; -use ron::de::from_str; +use bevy::log::error; use serde::Deserialize; use std::fmt; -use std::fs; pub fn volume(temperature: f32, pressure: f32, mass: f32, molar_mass: f32) -> f32 { // Volume (m³) of an ideal gas from its temperature (K), pressure (Pa), @@ -152,14 +150,3 @@ impl<'a> GasVolume<'a> { volume(self.temperature, self.pressure, self.mass, self.species.molar_mass) } } - -#[derive(Debug, Deserialize)] -struct GasConfig { - gases: Vec, -} - -fn load_gas_config(file_path: &str) -> Vec { - let content = fs::read_to_string(file_path).expect("Unable to read file"); - let config: GasConfig = from_str(&content).expect("Unable to parse RON"); - config.gases -} diff --git a/src/simulator/mod.rs b/src/simulator/mod.rs index 3ad8e01..f77a7af 100644 --- a/src/simulator/mod.rs +++ b/src/simulator/mod.rs @@ -21,6 +21,7 @@ pub struct SimulatorPlugins; impl PluginGroup for SimulatorPlugins { fn build(self) -> PluginGroupBuilder { PluginGroupBuilder::start::() + .add(atmosphere::AtmospherePlugin) .add(balloon::BalloonPlugin) } } @@ -45,6 +46,4 @@ impl PluginGroup for SimulatorPlugins { // let acceleration = net_force / total_dry_mass; // let ascent_rate = ascent_rate + acceleration * delta_t; // let altitude = altitude + ascent_rate * delta_t; - -// atmosphere.set_altitude(altitude); // } diff --git a/src/ui/balloon_designer.rs b/src/ui/balloon_designer.rs new file mode 100644 index 0000000..62b31a1 --- /dev/null +++ b/src/ui/balloon_designer.rs @@ -0,0 +1,67 @@ +use bevy::prelude::*; +use bevy_egui::{egui, EguiContexts}; +use crate::assets::PropertiesConfig; +use crate::AppState; + +pub struct BalloonDesignerPlugin; + +impl Plugin for BalloonDesignerPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_systems(Update, ui_system.run_if(in_state(AppState::Running))); + } +} + +#[derive(Resource)] +struct BalloonDesignerState { + diameter: f32, + thickness: f32, + selected_gas: usize, + selected_material: usize, +} + +impl Default for BalloonDesignerState { + fn default() -> Self { + Self { + diameter: 1.5, + thickness: 0.0001, + selected_gas: 0, + selected_material: 0, + } + } +} + +fn ui_system( + mut egui_context: EguiContexts, + mut state: ResMut, + properties: Res, +) { + egui::Window::new("Balloon Designer").show(egui_context.ctx_mut(), |ui| { + ui.label("Adjust the balloon properties:"); + + ui.add(egui::Slider::new(&mut state.diameter, 0.5..=5.0).text("Diameter (m)")); + ui.add(egui::Slider::new(&mut state.thickness, 0.00001..=0.01).text("Thickness (m)")); + + ui.label("Select Gas Species:"); + if let Some(gas) = properties.gases.get(state.selected_gas) { + egui::ComboBox::from_label("Gas") + .selected_text(&gas.name) + .show_ui(ui, |ui| { + for (index, gas) in properties.gases.iter().enumerate() { + ui.selectable_value(&mut state.selected_gas, index, &gas.name); + } + }); + } + + ui.label("Select Balloon Material:"); + if let Some(materials) = properties.materials.get(state.selected_material) { + egui::ComboBox::from_label("Material") + .selected_text(&materials.name) + .show_ui(ui, |ui| { + for (index, material) in properties.materials.iter().enumerate() { + ui.selectable_value(&mut state.selected_material, index, &material.name); + } + }); + } + }); +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index ccf4c2f..e49c869 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,20 +1,9 @@ use bevy::app::PluginGroupBuilder; use bevy::prelude::*; +use bevy_egui::EguiPlugin; +mod balloon_designer; mod shell; -// Import other UI-related modules here -// mod config_view; -// mod flight_view; -// mod stats_view; - -use shell::ShellPlugin; -// Use other UI-related plugins here -// use config_view::ConfigViewPlugin; -// use flight_view::FlightViewPlugin; -// use stats_view::StatsViewPlugin; - -// Re-export ShellPlugin and other UI-related items if needed -pub use shell::Shell; /// A plugin group that includes all interface-related plugins pub struct InterfacePlugins; @@ -22,29 +11,8 @@ pub struct InterfacePlugins; impl PluginGroup for InterfacePlugins { fn build(self) -> PluginGroupBuilder { PluginGroupBuilder::start::() - .add(ShellPlugin) - // Add other UI-related plugins here - // .add(ConfigViewPlugin) - // .add(FlightViewPlugin) - // .add(StatsViewPlugin) + .add(EguiPlugin) + .add(shell::ShellPlugin) + .add(balloon_designer::BalloonDesignerPlugin) } } - -/// Something to view in the monitor windows -pub trait View { - fn ui(&mut self, ui: &mut egui::Ui); -} - -/// Something to view -pub trait UiPanel { - /// Is the monitor enabled for this integration? - fn is_enabled(&self, _ctx: &egui::Context) -> bool { - true - } - - /// `&'static` so we can also use it as a key to store open/close state. - fn name(&self) -> &'static str; - - /// Show windows, etc - fn show(&mut self, ctx: &egui::Context, open: &mut bool); -} diff --git a/src/ui/shell.rs b/src/ui/shell.rs index d79e643..ff05d09 100644 --- a/src/ui/shell.rs +++ b/src/ui/shell.rs @@ -1,207 +1,21 @@ use bevy::prelude::*; -use bevy_egui::{egui, EguiContext, EguiPlugin}; - -use super::UiPanel; +use bevy_egui::{egui::{self, Modifiers, Ui}, EguiContexts}; pub struct ShellPlugin; impl Plugin for ShellPlugin { fn build(&self, app: &mut App) { - app.add_plugins(EguiPlugin) - .init_resource::() - .add_systems(Update, ui_system); - } -} - -#[derive(Resource)] -pub struct Shell { - screens: Screens, - config: Option, -} - -impl Default for Shell { - fn default() -> Self { - Self { - screens: Screens::default(), - config: None, - output: None, - run_handle: None, - } + app.add_systems(Update, ui_system); } } -fn ui_system(mut egui_context: ResMut, mut shell: ResMut) { - egui::SidePanel::left("mission_control_panel") - .resizable(false) - .default_width(150.0) - .show(egui_context.ctx_mut(), |ui| { - ui.vertical_centered(|ui| { - ui.heading("yahs"); - }); - - ui.separator(); - - use egui::special_emojis::GITHUB; - ui.hyperlink_to( - format!("{GITHUB} yahs on GitHub"), - "https://github.com/brickworks/yahs", - ); - ui.hyperlink_to( - format!("{GITHUB} @philiplinden"), - "https://github.com/philiplinden", - ); - - ui.separator(); - egui::widgets::global_dark_light_mode_buttons(ui); - ui.separator(); - shell.screen_list_ui(ui); - ui.separator(); - shell.sim_control_buttons(ui); - ui.separator(); - }); - - egui::TopBottomPanel::top("menu_bar").show(egui_context.ctx_mut(), |ui| { - egui::menu::bar(ui, |ui| { - file_menu_button(ui); - }); - }); - - shell.show_windows(egui_context.ctx_mut()); - - egui::TopBottomPanel::bottom("powered_by_bevy_egui").show(egui_context.ctx_mut(), |ui| { - ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { - powered_by_egui_and_bevy(ui); - }); +fn ui_system(mut egui_context: EguiContexts) { + egui::CentralPanel::default().show(egui_context.ctx_mut(), |ui| { + powered_by_egui_and_bevy(ui); + file_menu_button(ui); }); } -impl Shell { - /// Called once before the first frame. - pub fn new(_cc: &eframe::CreationContext<'_>) -> Self { - // This is also where you can customize the look and feel of egui using - // `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`. - Default::default() - } - - /// Show the app ui (menu bar and windows). - pub fn ui(&mut self, ctx: &Context) { - egui::SidePanel::left("mission_control_panel") - .resizable(false) - .default_width(150.0) - .show(ctx, |ui| { - ui.vertical_centered(|ui| { - ui.heading("yahs"); - }); - - ui.separator(); - - use egui::special_emojis::GITHUB; - ui.hyperlink_to( - format!("{GITHUB} yahs on GitHub"), - "https://github.com/brickworks/yahs", - ); - ui.hyperlink_to( - format!("{GITHUB} @philiplinden"), - "https://github.com/philiplinden", - ); - - ui.separator(); - egui::widgets::global_dark_light_mode_buttons(ui); - ui.separator(); - self.screen_list_ui(ui); - ui.separator(); - self.sim_control_buttons(ui); - ui.separator(); - }); - - egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { - egui::menu::bar(ui, |ui| { - file_menu_button(ui); - }); - }); - - self.show_windows(ctx); - } - - /// Show the open windows. - fn show_windows(&mut self, ctx: &Context) { - self.screens.windows(ctx); - } - - fn screen_list_ui(&mut self, ui: &mut egui::Ui) { - ScrollArea::vertical().show(ui, |ui| { - ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| { - self.screens.checkboxes(ui); - }); - }); - } - - fn sim_control_buttons(&mut self, ui: &mut egui::Ui) { - if ui.button("Simulate").clicked() { - if self.run_handle.is_some() { - panic!("Can't start again, sim already ran. Need to stop.") - } - let outpath = PathBuf::from("out.csv"); - let init_state = Arc::new(Mutex::new(SimOutput::default())); - if let Some(config) = self.config.clone() { - let output = init_state.clone(); - self.run_handle = Some(std::thread::spawn(move || { - AsyncSim::run_sim(config, output, outpath); - })); - } - } - } -} - -// ---------------------------------------------------------------------------- - -#[derive(Default)] -struct Screens { - screens: Vec>, - open: BTreeSet, -} - -impl Screens { - pub fn from_demos(screens: Vec>) -> Self { - let mut open = BTreeSet::new(); - open.insert(super::views::ConfigView::default().name().to_owned()); - - Self { screens, open } - } - - pub fn checkboxes(&mut self, ui: &mut Ui) { - let Self { screens, open } = self; - for screen in screens { - if screen.is_enabled(ui.ctx()) { - let mut is_open = open.contains(screen.name()); - ui.toggle_value(&mut is_open, screen.name()); - set_open(open, screen.name(), is_open); - } - } - } - - pub fn windows(&mut self, ctx: &Context) { - let Self { screens, open } = self; - for screen in screens { - let mut is_open = open.contains(screen.name()); - screen.show(ctx, &mut is_open); - set_open(open, screen.name(), is_open); - } - } -} - -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "serde", serde(default))] -fn set_open(open: &mut BTreeSet, key: &'static str, is_open: bool) { - if is_open { - if !open.contains(key) { - open.insert(key.to_owned()); - } - } else { - open.remove(key); - } -} - fn powered_by_egui_and_bevy(ui: &mut egui::Ui) { ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 0.0; @@ -235,7 +49,6 @@ fn file_menu_button(ui: &mut Ui) { ui.menu_button("View", |ui| { ui.set_min_width(220.0); - ui.style_mut().wrap = Some(false); // On the web the browser controls the zoom #[cfg(not(target_arch = "wasm32"))] @@ -274,64 +87,22 @@ fn file_menu_button(ui: &mut Ui) { }); } -// ---------------------------------------------------------------------------- - -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "serde", serde(default))] -struct Screens { - #[cfg_attr(feature = "serde", serde(skip))] - screens: Vec>, - - open: BTreeSet, -} - -impl Default for Screens { - fn default() -> Self { - Self::from_demos(vec![ - Box::::default(), - Box::::default(), - Box::::default(), - ]) - } -} - -impl Screens { - pub fn from_demos(screens: Vec>) -> Self { - let mut open = BTreeSet::new(); - open.insert(super::views::ConfigView::default().name().to_owned()); - - Self { screens, open } - } - - pub fn checkboxes(&mut self, ui: &mut Ui) { - let Self { screens, open } = self; - for screen in screens { - if screen.is_enabled(ui.ctx()) { - let mut is_open = open.contains(screen.name()); - ui.toggle_value(&mut is_open, screen.name()); - set_open(open, screen.name(), is_open); - } - } - } - - pub fn windows(&mut self, ctx: &Context) { - let Self { screens, open } = self; - for screen in screens { - let mut is_open = open.contains(screen.name()); - screen.show(ctx, &mut is_open); - set_open(open, screen.name(), is_open); - } - } -} - -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "serde", serde(default))] -fn set_open(open: &mut BTreeSet, key: &'static str, is_open: bool) { - if is_open { - if !open.contains(key) { - open.insert(key.to_owned()); - } - } else { - open.remove(key); - } -} + // pub fn checkboxes(&mut self, ui: &mut Ui) { + // let Self { screens, open } = self; + // for screen in screens { + // if screen.is_enabled(ui.ctx()) { + // let mut is_open = open.contains(screen.name()); + // ui.toggle_value(&mut is_open, screen.name()); + // set_open(open, screen.name(), is_open); + // } + // } + // } + + // pub fn windows(&mut self, ctx: &Context) { + // let Self { screens, open } = self; + // for screen in screens { + // let mut is_open = open.contains(screen.name()); + // screen.show(ctx, &mut is_open); + // set_open(open, screen.name(), is_open); + // } + // } diff --git a/src/ui/views.rs b/src/ui/views.rs index cc65201..8a864f0 100644 --- a/src/ui/views.rs +++ b/src/ui/views.rs @@ -2,7 +2,7 @@ use egui::*; use egui_plot::{ Bar, BarChart, BoxElem, BoxPlot, BoxSpread, Legend, Line, Plot, }; -use log::error; +use bevy::log::error; use crate::gui::View; use crate::original::config::{self, Config}; From 9523bcd818d40a5177688ad24d771c9bf1b82de8 Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Fri, 8 Nov 2024 15:25:24 -0500 Subject: [PATCH 08/47] bevy: round 5 --- Cargo.toml | 76 ++++++++++++++- assets/{ => configs}/properties.ron | 0 assets/{ => configs}/setup.ron | 0 assets/textures/splash.png | Bin 0 -> 15391 bytes docs/devlog.md | 27 +++++ src/assets.rs | 103 ++++++++++++++++++-- src/dev_tools.rs | 30 ++++++ src/lib.rs | 102 +++++++++++++++++++ src/main.rs | 41 ++------ src/simulator/balloon.rs | 2 +- src/simulator/gas.rs | 4 +- src/ui/{balloon_designer.rs => designer.rs} | 2 +- src/ui/mod.rs | 6 +- src/ui/splash.rs | 42 ++++++++ 14 files changed, 383 insertions(+), 52 deletions(-) rename assets/{ => configs}/properties.ron (100%) rename assets/{ => configs}/setup.ron (100%) create mode 100644 assets/textures/splash.png create mode 100644 src/dev_tools.rs create mode 100644 src/lib.rs rename src/ui/{balloon_designer.rs => designer.rs} (98%) create mode 100644 src/ui/splash.rs diff --git a/Cargo.toml b/Cargo.toml index 7da7bdb..4409f3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,11 +2,29 @@ name = "yahs" description = "Yet Another HAB Simulator" authors = ["Philip Linden "] -version = "0.3.0" +version = "0.4.0" edition = "2021" readme = "README.md" license-file = "LICENSE" +[features] +default = [ + # Default to a native dev build. + "dev_native", +] +dev = [ + # Improve compile times for dev builds by linking Bevy as a dynamic library. + "bevy/dynamic_linking", + "bevy/bevy_dev_tools", +] +dev_native = [ + "dev", + # Enable asset hot reloading for native dev builds. + "bevy/file_watcher", + # Enable embedded asset hot reloading for native dev builds. + "bevy/embedded_watcher", +] + [dependencies] pretty_env_logger = "0.5.0" serde = { version = "1.0.214", features = ["derive"] } @@ -21,3 +39,59 @@ bevy_common_assets = { version = "0.11.0", features = ["ron"] } [[bin]] name = "yahs" path = "src/main.rs" + +# ----------------------------------------------------------------------------- +# Some Bevy optimizations +# ----------------------------------------------------------------------------- + +# Idiomatic Bevy code often triggers these lints, and the CI workflow treats +# them as errors. In some cases they may still signal poor code quality however, +# so consider commenting out these lines. +[lints.clippy] +# Bevy supplies arguments to systems via dependency injection, so it's natural +# for systems to request more than 7 arguments -- which triggers this lint. +too_many_arguments = "allow" +# Queries that access many components may trigger this lint. +type_complexity = "allow" + + +# Compile with Performance Optimizations: +# https://bevyengine.org/learn/quick-start/getting-started/setup/#compile-with-performance-optimizations + +# Enable a small amount of optimization in the dev profile. +[profile.dev] +opt-level = 1 + +# Enable a large amount of optimization in the dev profile for dependencies. +[profile.dev.package."*"] +opt-level = 3 + +# Remove expensive debug assertions due to +# +[profile.dev.package.wgpu-types] +debug-assertions = false + +# The default profile is optimized for Wasm builds because that's what [Trunk +# reads](https://github.com/trunk-rs/trunk/issues/605). Optimize for size in the +# wasm-release profile to reduce load times and bandwidth usage on web. +[profile.release] +# Compile the entire crate as one unit. Slows compile times, marginal +# improvements. +codegen-units = 1 +# Do a second optimization pass over the entire program, including dependencies. +# Slows compile times, marginal improvements. +lto = "thin" +# Optimize with size in mind (also try "z", sometimes it is better). Slightly +# slows compile times, great improvements to file size and runtime performance. +opt-level = "s" +# Strip all debugging information from the binary to slightly reduce file size. +strip = "debuginfo" + +# Override some settings for native builds. +[profile.release-native] +# Default to release profile values. +inherits = "release" +# Optimize with performance in mind. +opt-level = 3 +# Keep debug information in the binary. +strip = "none" diff --git a/assets/properties.ron b/assets/configs/properties.ron similarity index 100% rename from assets/properties.ron rename to assets/configs/properties.ron diff --git a/assets/setup.ron b/assets/configs/setup.ron similarity index 100% rename from assets/setup.ron rename to assets/configs/setup.ron diff --git a/assets/textures/splash.png b/assets/textures/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..d984dbb1ad171bc10d49a29dea8d679be3d64236 GIT binary patch literal 15391 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 literal 0 HcmV?d00001 diff --git a/docs/devlog.md b/docs/devlog.md index 931e6c5..611bdbb 100644 --- a/docs/devlog.md +++ b/docs/devlog.md @@ -1,5 +1,32 @@ # development log +## 2024-11-08 + +Focusing on getting the app running as a simple splash screen followed by a +loading screen followed by the running application. Loading the configuration +file and setting up the simulation from there. + +I am having trouble getting the Bevy asset server to load the configuration +file. It seems to be advancing to the running state before the configuration +file is loaded. I added a splash screen to practice with state transitions. + +I also added dev tools provided by the Bevy Quickstart Template +([link](https://github.com/TheBevyFlock/bevy_new_2d/blob/main/src/dev_tools.rs)) +to help with debugging. This is toggled with the `F3` key by default, and only +added when the `dev` feature is enabled (it is not enabled by default when +building with cargo and omitted from the release build). I borrowed some other +patterns from the Bevy Quickstart Template for the asset tracking and a few +small things. + +### Changelog - 2024-11-08 + +- Added splash screen to the application. +- Changed the generic asset loader to a configuration loader for now. +- Added asset tracking plugin. +- Added dev tools provided by the Bevy Quickstart Template. +- Added `dev` feature flag and Bevy build optimiztions to `Cargo.toml`. +- Added `lib.rs` and moved some things around to clean up the root directory. + ## 2024-11-07 I am switching to Bevy for the simulation. Bevy is a "bevy engine" which is A framework for building games and simulations. It allows for high performance, diff --git a/src/assets.rs b/src/assets.rs index d763fcb..160d1d6 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -1,46 +1,129 @@ use bevy::prelude::*; +use std::collections::VecDeque; use bevy_common_assets::ron::RonAssetPlugin; use serde::Deserialize; use crate::simulator::{balloon::BalloonMaterial, gas::GasSpecies}; use crate::AppState; -#[derive(Resource, Asset, TypePath, Debug, Deserialize)] +/// Configuration for the properties of gases and materials. +#[derive(Resource, Asset, Debug, Deserialize, Reflect)] pub struct PropertiesConfig { pub gases: Vec, pub materials: Vec, } +impl Default for PropertiesConfig { + fn default() -> Self { + Self { gases: vec![], materials: vec![] } + } +} + +/// Asset handle for the properties configuration asset. #[derive(Resource)] pub struct PropertiesConfigHandle(Handle); -pub struct AssetLoaderPlugin; +/// Plugin for loading configuration. +pub struct ConfigLoaderPlugin; -impl Plugin for AssetLoaderPlugin { +impl Plugin for ConfigLoaderPlugin { fn build(&self, app: &mut App) { app.add_plugins((RonAssetPlugin::::new(&["ron"]),)) - .add_systems(Startup, setup_asset_loader) - .add_systems(OnEnter(AppState::Loading), load_assets); + .insert_resource(PropertiesConfig::default()) + .add_systems(Startup, setup_config_loader) + .add_systems(OnEnter(AppState::Loading), load_configs); } } -fn setup_asset_loader(asset_server: Res, mut commands: Commands) { - commands.insert_resource(PropertiesConfigHandle(asset_server.load("properties.ron"))); +/// Sets up the configuration loader for the properties configuration file. +fn setup_config_loader(asset_server: Res, mut commands: Commands) { + info!("Setting up configuration loader"); + commands.insert_resource(PropertiesConfigHandle(asset_server.load("configs/properties.ron"))); + info!("Configuration loader setup complete"); } -fn load_assets( +/// Loads the configuration and transitions to the running state. +fn load_configs( properties_handle: Res, properties: Res>, mut commands: Commands, mut state: ResMut>, ) { if let Some(properties_config) = properties.get(&properties_handle.0) { - // Insert the loaded properties as a resource + info!("Configuration loaded successfully"); commands.insert_resource(PropertiesConfig { gases: properties_config.gases.clone(), materials: properties_config.materials.clone(), }); - // Transition to the Running state state.set(AppState::Running); + info!("Transitioning to Running state"); + } else { + warn!("Configuration not yet loaded"); } } + + + +/// A high-level way to load collections of asset handles as resources. +pub struct AssetTrackingPlugin; + +impl Plugin for AssetTrackingPlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + app.add_systems(PreUpdate, load_resource_assets); + } +} + +pub trait LoadResource { + /// This will load the [`Resource`] as an [`Asset`]. When all of its asset dependencies + /// have been loaded, it will be inserted as a resource. This ensures that the resource only + /// exists when the assets are ready. + fn load_resource(&mut self) -> &mut Self; +} + +impl LoadResource for App { + fn load_resource(&mut self) -> &mut Self { + self.init_asset::(); + let world = self.world_mut(); + let value = T::from_world(world); + let assets = world.resource::(); + let handle = assets.add(value); + let mut handles = world.resource_mut::(); + handles + .waiting + .push_back((handle.untyped(), |world, handle| { + let assets = world.resource::>(); + if let Some(value) = assets.get(handle.id().typed::()) { + world.insert_resource(value.clone()); + } + })); + self + } +} + +/// A function that inserts a loaded resource. +type InsertLoadedResource = fn(&mut World, &UntypedHandle); + +#[derive(Resource, Default)] +struct ResourceHandles { + // Use a queue for waiting assets so they can be cycled through and moved to + // `finished` one at a time. + waiting: VecDeque<(UntypedHandle, InsertLoadedResource)>, + finished: Vec, +} + +fn load_resource_assets(world: &mut World) { + world.resource_scope(|world, mut resource_handles: Mut| { + world.resource_scope(|world, assets: Mut| { + for _ in 0..resource_handles.waiting.len() { + let (handle, insert_fn) = resource_handles.waiting.pop_front().unwrap(); + if assets.is_loaded_with_dependencies(&handle) { + insert_fn(world, &handle); + resource_handles.finished.push(handle); + } else { + resource_handles.waiting.push_back((handle, insert_fn)); + } + } + }); + }); +} diff --git a/src/dev_tools.rs b/src/dev_tools.rs new file mode 100644 index 0000000..7f387db --- /dev/null +++ b/src/dev_tools.rs @@ -0,0 +1,30 @@ +//! Development tools for the game. This plugin is only enabled in dev builds. + +use bevy::{ + dev_tools::{ + states::log_transitions, + ui_debug_overlay::{DebugUiPlugin, UiDebugOptions}, + }, + input::common_conditions::input_just_pressed, + prelude::*, +}; + +use crate::screens::Screen; + +pub(super) fn plugin(app: &mut App) { + // Log `Screen` state transitions. + app.add_systems(Update, log_transitions::); + + // Toggle the debug overlay for UI. + app.add_plugins(DebugUiPlugin); + app.add_systems( + Update, + toggle_debug_ui.run_if(input_just_pressed(TOGGLE_KEY)), + ); +} + +const TOGGLE_KEY: KeyCode = KeyCode::Backquote; + +fn toggle_debug_ui(mut options: ResMut) { + options.toggle(); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6b1630c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,102 @@ +mod assets; +mod simulator; +mod ui; + +#[cfg(feature = "dev")] +mod dev_tools; + +use bevy::{asset::AssetMetaCheck, prelude::*}; + +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)] +pub enum AppState { + #[default] + Splash, + Loading, + Running, +} + +impl Plugin for AppPlugin { + fn build(&self, app: &mut App) { + setup_pretty_logs(); + + // Order new `AppStep` variants by adding them here: + app.configure_sets( + Update, + (AppSet::TickTimers, AppSet::RecordInput, AppSet::Update).chain(), + ); + + // Spawn the main camera. + app.add_systems(Startup, spawn_camera); + + // Add Bevy plugins. + app.add_plugins( + DefaultPlugins + .set(AssetPlugin { + // Wasm builds will check for meta files (that don't exist) + // if this isn't set. This causes errors and even panics on + // web build on itch. See + // https://github.com/bevyengine/bevy_github_ci_template/issues/48. + meta_check: AssetMetaCheck::Never, + ..default() + }) + .set(WindowPlugin { + primary_window: Window { + title: "🎈".to_string(), + canvas: Some("#bevy".to_string()), + fit_canvas_to_parent: true, + prevent_default_event_handling: true, + ..default() + } + .into(), + ..default() + }), + ); + + // Add other plugins. + app.add_plugins(( + ui::InterfacePlugins, + assets::AssetTrackingPlugin, + assets::ConfigLoaderPlugin, + simulator::SimulatorPlugins, + )); + + // Enable dev tools for dev builds. + #[cfg(feature = "dev")] + app.add_plugins(dev_tools::plugin); + } +} + +fn setup_pretty_logs() { + // look for the RUST_LOG env var or default to "info" + let rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_owned()); + std::env::set_var("RUST_LOG", rust_log); + // initialize pretty print logger + pretty_env_logger::init(); +} + +/// High-level groupings of systems for the app in the `Update` schedule. When +/// adding a new variant, make sure to order it in the `configure_sets` call +/// above. +#[derive(SystemSet, Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)] +enum AppSet { + /// Tick timers. + TickTimers, + /// Record player input. + RecordInput, + /// Do everything else (consider splitting this into further variants). + Update, +} + +fn spawn_camera(mut commands: Commands) { + commands.spawn(( + Name::new("Camera"), + Camera3dBundle::default(), + // Render all UI to this camera. Not strictly necessary since we only + // use one camera, but if we don't use this component, our UI will + // disappear as soon as we add another camera. This includes indirect + // ways of adding cameras like using [ui node + // outlines](https://bevyengine.org/news/bevy-0-14/#ui-node-outline-gizmos) + // for debugging. So it's good to have this here for future-proofing. + IsDefaultUiCamera, + )); +} diff --git a/src/main.rs b/src/main.rs index bb1f5ae..5291dce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,38 +1,9 @@ -mod ui; -mod simulator; -mod assets; -use bevy::prelude::*; - -#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)] -enum AppState { - #[default] - Loading, - Running, -} +// Disable console on Windows for non-dev builds. +#![cfg_attr(not(feature = "dev"), windows_subsystem = "windows")] -fn main() { - setup_pretty_logs(); - App::new() - .init_state::() - .add_plugins(( - DefaultPlugins.set(WindowPlugin { - primary_window: Some(Window { - title: "🎈".to_string(), - ..default() - }), - ..default() - }), - ui::InterfacePlugins, - simulator::SimulatorPlugins, - assets::AssetLoaderPlugin, - )) - .run(); -} +use bevy::prelude::*; +use yahs::AppPlugin; -fn setup_pretty_logs() { - // look for the RUST_LOG env var or default to "info" - let rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_owned()); - std::env::set_var("RUST_LOG", rust_log); - // initialize pretty print logger - pretty_env_logger::init(); +fn main() -> AppExit { + App::new().add_plugins(AppPlugin).run() } diff --git a/src/simulator/balloon.rs b/src/simulator/balloon.rs index 5859f39..a2e2418 100644 --- a/src/simulator/balloon.rs +++ b/src/simulator/balloon.rs @@ -250,7 +250,7 @@ pub fn projected_spherical_area(volume: f32) -> f32 { // Source: https://www.matweb.com/ // ---------------------------------------------------------------------------- -#[derive(Debug, Deserialize, Clone, PartialEq)] +#[derive(Debug, Deserialize, Clone, PartialEq, Reflect)] pub struct BalloonMaterial { pub name: String, pub max_temperature: f32, // temperature (K) where the given material fails diff --git a/src/simulator/gas.rs b/src/simulator/gas.rs index 9066897..1c370d5 100644 --- a/src/simulator/gas.rs +++ b/src/simulator/gas.rs @@ -19,7 +19,7 @@ #![allow(dead_code)] use super::constants::{R, STANDARD_PRESSURE, STANDARD_TEMPERATURE}; -use bevy::log::error; +use bevy::{prelude::*, log::error}; use serde::Deserialize; use std::fmt; @@ -36,7 +36,7 @@ pub fn density(temperature: f32, pressure: f32, molar_mass: f32) -> f32 { } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, Reflect)] pub struct GasSpecies { pub name: String, pub abbreviation: String, diff --git a/src/ui/balloon_designer.rs b/src/ui/designer.rs similarity index 98% rename from src/ui/balloon_designer.rs rename to src/ui/designer.rs index 62b31a1..c3cc7d5 100644 --- a/src/ui/balloon_designer.rs +++ b/src/ui/designer.rs @@ -1,6 +1,6 @@ use bevy::prelude::*; use bevy_egui::{egui, EguiContexts}; -use crate::assets::PropertiesConfig; +use crate::config::PropertiesConfig; use crate::AppState; pub struct BalloonDesignerPlugin; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index e49c869..34afe65 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -2,8 +2,9 @@ use bevy::app::PluginGroupBuilder; use bevy::prelude::*; use bevy_egui::EguiPlugin; -mod balloon_designer; +mod designer; mod shell; +mod splash; /// A plugin group that includes all interface-related plugins pub struct InterfacePlugins; @@ -13,6 +14,7 @@ impl PluginGroup for InterfacePlugins { PluginGroupBuilder::start::() .add(EguiPlugin) .add(shell::ShellPlugin) - .add(balloon_designer::BalloonDesignerPlugin) + .add(designer::BalloonDesignerPlugin) + .add(splash::SplashPlugin) } } diff --git a/src/ui/splash.rs b/src/ui/splash.rs new file mode 100644 index 0000000..b26b79c --- /dev/null +++ b/src/ui/splash.rs @@ -0,0 +1,42 @@ +use bevy::prelude::*; +use crate::AppState; + +pub struct SplashPlugin; + +impl Plugin for SplashPlugin { + fn build(&self, app: &mut App) { + app.insert_resource(ClearColor(SPLASH_BACKGROUND_COLOR)) + .add_systems(OnEnter(AppState::Splash), setup_splash) + .add_systems(Update, update_splash.run_if(in_state(AppState::Splash))) + .add_systems(OnExit(AppState::Splash), cleanup_splash); + } +} + +const SPLASH_BACKGROUND_COLOR: Color = Color::srgb(0.157, 0.157, 0.157); +const SPLASH_DURATION_SECS: f32 = 1.8; + +fn setup_splash(mut commands: Commands, asset_server: Res) { + let texture_handle = asset_server.load("textures/splash.png"); + commands.spawn(SpriteBundle { + texture: texture_handle, + ..default() + }); +} + +fn update_splash( + time: Res