diff --git a/.gitignore b/.gitignore index 27351d5..654038d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ target package-lock.json **/*.csv .history -.vscode/ \ No newline at end of file +.vscode/ +*.autosave.json \ No newline at end of file diff --git a/benchmarks2d/all_benchmarks2.rs b/benchmarks2d/all_benchmarks2.rs index c3ba5bd..872f148 100644 --- a/benchmarks2d/all_benchmarks2.rs +++ b/benchmarks2d/all_benchmarks2.rs @@ -3,8 +3,6 @@ #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; -use inflector::Inflector; - use rapier_testbed2d::{Testbed, TestbedApp}; use std::cmp::Ordering; @@ -46,11 +44,6 @@ fn demo_name_from_url() -> Option { #[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))] pub fn main() { - let demo = demo_name_from_command_line() - .or_else(demo_name_from_url) - .unwrap_or_default() - .to_camel_case(); - let mut builders: Vec<(_, fn(&mut Testbed))> = vec![ ("Balls", balls2::init_world), ("Boxes", boxes2::init_world), @@ -74,11 +67,7 @@ pub fn main() { (false, true) => Ordering::Less, }); - let i = builders - .iter() - .position(|builder| builder.0.to_camel_case().as_str() == demo.as_str()) - .unwrap_or(0); - let testbed = TestbedApp::from_builders(i, builders); + let testbed = TestbedApp::from_builders(builders); testbed.run() } diff --git a/benchmarks3d/all_benchmarks3.rs b/benchmarks3d/all_benchmarks3.rs index c1e0da2..d04b294 100644 --- a/benchmarks3d/all_benchmarks3.rs +++ b/benchmarks3d/all_benchmarks3.rs @@ -84,12 +84,12 @@ pub fn main() { .iter() .position(|builder| builder.0.to_camel_case().as_str() == demo.as_str()) { - TestbedApp::from_builders(0, vec![builders[i]]).run() + TestbedApp::from_builders(vec![builders[i]]).run() } else { eprintln!("Invalid example to run provided: '{}'", demo); } } - Command::RunAll => TestbedApp::from_builders(0, builders).run(), + Command::RunAll => TestbedApp::from_builders(builders).run(), Command::List => { for builder in &builders { println!("{}", builder.0.to_camel_case()) diff --git a/crates/rapier_testbed2d-f64/Cargo.toml b/crates/rapier_testbed2d-f64/Cargo.toml index 14285a6..7ff1e02 100644 --- a/crates/rapier_testbed2d-f64/Cargo.toml +++ b/crates/rapier_testbed2d-f64/Cargo.toml @@ -61,6 +61,9 @@ bevy_pbr = "0.15" bevy_sprite = "0.15" profiling = "1.0" puffin_egui = { version = "0.29", optional = true } +serde_json = "1" +serde = { version = "1.0.215", features = ["derive"] } + # Dependencies for native only. [target.'cfg(not(target_arch = "wasm32"))'.dependencies] @@ -75,6 +78,7 @@ bevy = { version = "0.15", default-features = false, features = [ "bevy_render", "bevy_pbr", "bevy_gizmos", + "serialize" ] } # Dependencies for WASM only. diff --git a/crates/rapier_testbed2d/Cargo.toml b/crates/rapier_testbed2d/Cargo.toml index d8649b5..de2512f 100644 --- a/crates/rapier_testbed2d/Cargo.toml +++ b/crates/rapier_testbed2d/Cargo.toml @@ -61,6 +61,8 @@ bevy_pbr = "0.15" bevy_sprite = "0.15" profiling = "1.0" puffin_egui = { version = "0.29", optional = true } +serde = { version = "1.0.215", features = ["derive"] } +serde_json = "1" # Dependencies for native only. [target.'cfg(not(target_arch = "wasm32"))'.dependencies] @@ -75,6 +77,7 @@ bevy = { version = "0.15", default-features = false, features = [ "bevy_render", "bevy_pbr", "bevy_gizmos", + "serialize" ] } # Dependencies for WASM only. diff --git a/crates/rapier_testbed3d-f64/Cargo.toml b/crates/rapier_testbed3d-f64/Cargo.toml index 0c896ab..d000848 100644 --- a/crates/rapier_testbed3d-f64/Cargo.toml +++ b/crates/rapier_testbed3d-f64/Cargo.toml @@ -56,6 +56,7 @@ bincode = "1" md5 = "0.7" Inflector = "0.11" serde = { version = "1", features = ["derive"] } +serde_json = "1" bevy_egui = "0.31" bevy_ecs = "0.15" bevy_core_pipeline = "0.15" @@ -76,6 +77,7 @@ bevy = { version = "0.15", default-features = false, features = [ "bevy_render", "bevy_pbr", "bevy_gizmos", + "serialize" ] } # Dependencies for WASM only. @@ -90,6 +92,7 @@ bevy = { version = "0.15", default-features = false, features = [ "bevy_render", "bevy_pbr", "bevy_gizmos", + "serialize" ] } #bevy_webgl2 = "0.5" diff --git a/crates/rapier_testbed3d/Cargo.toml b/crates/rapier_testbed3d/Cargo.toml index 4f9a8a1..5533bea 100644 --- a/crates/rapier_testbed3d/Cargo.toml +++ b/crates/rapier_testbed3d/Cargo.toml @@ -57,6 +57,7 @@ bincode = "1" md5 = "0.7" Inflector = "0.11" serde = { version = "1", features = ["derive"] } +serde_json = "1" bevy_egui = "0.31" bevy_ecs = "0.15" bevy_core_pipeline = "0.15" @@ -77,6 +78,7 @@ bevy = { version = "0.15", default-features = false, features = [ "bevy_render", "bevy_pbr", "bevy_gizmos", + "serialize" ] } # Dependencies for WASM only. diff --git a/examples2d/Cargo.toml b/examples2d/Cargo.toml index a5e81f6..25e339a 100644 --- a/examples2d/Cargo.toml +++ b/examples2d/Cargo.toml @@ -14,7 +14,6 @@ enhanced-determinism = ["rapier2d/enhanced-determinism"] [dependencies] rand = "0.8" -Inflector = "0.11" lyon = "0.17" usvg = "0.14" diff --git a/examples2d/all_examples2.rs b/examples2d/all_examples2.rs index 74bd89d..4926b4e 100644 --- a/examples2d/all_examples2.rs +++ b/examples2d/all_examples2.rs @@ -3,8 +3,6 @@ #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; -use inflector::Inflector; - use rapier_testbed2d::{Testbed, TestbedApp}; use std::cmp::Ordering; @@ -45,38 +43,8 @@ mod s2d_pyramid; mod sensor2; mod trimesh2; -fn demo_name_from_command_line() -> Option { - let mut args = std::env::args(); - - while let Some(arg) = args.next() { - if &arg[..] == "--example" { - return args.next(); - } - } - - None -} - -#[cfg(target_arch = "wasm32")] -fn demo_name_from_url() -> Option { - None - // let window = stdweb::web::window(); - // let hash = window.location()?.search().ok()?; - // Some(hash[1..].to_string()) -} - -#[cfg(not(any(target_arch = "wasm32")))] -fn demo_name_from_url() -> Option { - None -} - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))] pub fn main() { - let demo = demo_name_from_command_line() - .or_else(demo_name_from_url) - .unwrap_or_default() - .to_camel_case(); - let mut builders: Vec<(_, fn(&mut Testbed))> = vec![ ("Add remove", add_remove2::init_world), ("CCD", ccd2::init_world), @@ -126,11 +94,7 @@ pub fn main() { (false, true) => Ordering::Less, }); - let i = builders - .iter() - .position(|builder| builder.0.to_camel_case().as_str() == demo.as_str()) - .unwrap_or(0); - let testbed = TestbedApp::from_builders(i, builders); + let testbed = TestbedApp::from_builders(builders); testbed.run() } diff --git a/examples2d/s2d_pyramid.rs b/examples2d/s2d_pyramid.rs index c35fe63..2612d3f 100644 --- a/examples2d/s2d_pyramid.rs +++ b/examples2d/s2d_pyramid.rs @@ -21,7 +21,9 @@ pub fn init_world(testbed: &mut Testbed) { /* * Create the cubes */ - let base_count = 100; + const BASE_COUNT_SETTING: &str = "# of basis cubes"; + let settings = testbed.example_settings_mut(); + let base_count = settings.get_or_set_u32(BASE_COUNT_SETTING, 100, 2..=200); let h = 0.5; let shift = 1.0 * h; diff --git a/examples3d-f64/Cargo.toml b/examples3d-f64/Cargo.toml index 65e7952..577810c 100644 --- a/examples3d-f64/Cargo.toml +++ b/examples3d-f64/Cargo.toml @@ -14,7 +14,6 @@ enhanced-determinism = ["rapier3d-f64/enhanced-determinism"] [dependencies] rand = "0.8" getrandom = { version = "0.2", features = ["js"] } -Inflector = "0.11" wasm-bindgen = "0.2" obj-rs = { version = "0.7", default-features = false } bincode = "1" diff --git a/examples3d-f64/all_examples3-f64.rs b/examples3d-f64/all_examples3-f64.rs index 3bece18..b7de076 100644 --- a/examples3d-f64/all_examples3-f64.rs +++ b/examples3d-f64/all_examples3-f64.rs @@ -5,49 +5,13 @@ use wasm_bindgen::prelude::*; extern crate rapier3d_f64 as rapier3d; extern crate rapier_testbed3d_f64 as rapier_testbed3d; -use inflector::Inflector; - use rapier_testbed3d::{Testbed, TestbedApp}; use std::cmp::Ordering; mod debug_serialized3; -fn demo_name_from_command_line() -> Option { - let mut args = std::env::args(); - - while let Some(arg) = args.next() { - if &arg[..] == "--example" { - return args.next(); - } - } - - None -} - -#[cfg(target_arch = "wasm32")] -fn demo_name_from_url() -> Option { - None - // let window = stdweb::web::window(); - // let hash = window.location()?.search().ok()?; - // if hash.len() > 0 { - // Some(hash[1..].to_string()) - // } else { - // None - // } -} - -#[cfg(not(target_arch = "wasm32"))] -fn demo_name_from_url() -> Option { - None -} - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))] pub fn main() { - let demo = demo_name_from_command_line() - .or_else(demo_name_from_url) - .unwrap_or_default() - .to_camel_case(); - let mut builders: Vec<(_, fn(&mut Testbed))> = vec![("(Debug) serialized", debug_serialized3::init_world)]; @@ -58,11 +22,6 @@ pub fn main() { (false, true) => Ordering::Less, }); - let i = builders - .iter() - .position(|builder| builder.0.to_camel_case().as_str() == demo.as_str()) - .unwrap_or(0); - - let testbed = TestbedApp::from_builders(i, builders); + let testbed = TestbedApp::from_builders(builders); testbed.run() } diff --git a/examples3d/Cargo.toml b/examples3d/Cargo.toml index 268040b..223ab21 100644 --- a/examples3d/Cargo.toml +++ b/examples3d/Cargo.toml @@ -15,7 +15,6 @@ enhanced-determinism = ["rapier3d/enhanced-determinism"] [dependencies] rand = "0.8" getrandom = { version = "0.2", features = ["js"] } -Inflector = "0.11" wasm-bindgen = "0.2" obj-rs = { version = "0.7", default-features = false } serde = "1" diff --git a/examples3d/all_examples3.rs b/examples3d/all_examples3.rs index 72a3cf7..a49414e 100644 --- a/examples3d/all_examples3.rs +++ b/examples3d/all_examples3.rs @@ -3,8 +3,6 @@ #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; -use inflector::Inflector; - use rapier_testbed3d::{Testbed, TestbedApp}; use std::cmp::Ordering; @@ -59,42 +57,8 @@ mod urdf3; mod vehicle_controller3; mod vehicle_joints3; -fn demo_name_from_command_line() -> Option { - let mut args = std::env::args(); - - while let Some(arg) = args.next() { - if &arg[..] == "--example" { - return args.next(); - } - } - - None -} - -#[cfg(target_arch = "wasm32")] -fn demo_name_from_url() -> Option { - None - // let window = stdweb::web::window(); - // let hash = window.location()?.search().ok()?; - // if hash.len() > 0 { - // Some(hash[1..].to_string()) - // } else { - // None - // } -} - -#[cfg(not(target_arch = "wasm32"))] -fn demo_name_from_url() -> Option { - None -} - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))] pub fn main() { - let demo = demo_name_from_command_line() - .or_else(demo_name_from_url) - .unwrap_or_default() - .to_camel_case(); - let mut builders: Vec<(_, fn(&mut Testbed))> = vec![ ("Character controller", character_controller3::init_world), ("Fountain", fountain3::init_world), @@ -173,11 +137,6 @@ pub fn main() { (false, true) => Ordering::Less, }); - let i = builders - .iter() - .position(|builder| builder.0.to_camel_case().as_str() == demo.as_str()) - .unwrap_or(0); - - let testbed = TestbedApp::from_builders(i, builders); + let testbed = TestbedApp::from_builders(builders); testbed.run() } diff --git a/examples3d/all_examples3_wasm.rs b/examples3d/all_examples3_wasm.rs index 6b70c38..5a31874 100644 --- a/examples3d/all_examples3_wasm.rs +++ b/examples3d/all_examples3_wasm.rs @@ -3,8 +3,6 @@ #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; -use inflector::Inflector; - use rapier_testbed3d::{Testbed, TestbedApp}; use std::cmp::Ordering; @@ -124,11 +122,6 @@ pub fn main() { (false, true) => Ordering::Less, }); - let i = builders - .iter() - .position(|builder| builder.0.to_camel_case().as_str() == demo.as_str()) - .unwrap_or(0); - - let testbed = TestbedApp::from_builders(i, builders); + let testbed = TestbedApp::from_builders(builders); testbed.run() } diff --git a/src_testbed/camera2d.rs b/src_testbed/camera2d.rs index ae5c163..78bff9c 100644 --- a/src_testbed/camera2d.rs +++ b/src_testbed/camera2d.rs @@ -9,7 +9,7 @@ use bevy::render::camera::Camera; const LINE_TO_PIXEL_RATIO: f32 = 0.1; -#[derive(Component)] +#[derive(Component, PartialEq, Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct OrbitCamera { pub zoom: f32, pub center: Vec3, diff --git a/src_testbed/camera3d.rs b/src_testbed/camera3d.rs index c9ad2da..ce35d8d 100644 --- a/src_testbed/camera3d.rs +++ b/src_testbed/camera3d.rs @@ -11,7 +11,7 @@ use std::ops::RangeInclusive; const LINE_TO_PIXEL_RATIO: f32 = 0.1; -#[derive(Component)] +#[derive(Component, PartialEq, Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct OrbitCamera { pub x: f32, pub y: f32, diff --git a/src_testbed/graphics.rs b/src_testbed/graphics.rs index 355f80f..4e27b2b 100644 --- a/src_testbed/graphics.rs +++ b/src_testbed/graphics.rs @@ -13,6 +13,7 @@ use rapier::math::{Isometry, Real, Vector}; //#[cfg(feature = "dim2")] //use crate::objects::polyline::Polyline; // use crate::objects::mesh::Mesh; +use crate::testbed::TestbedStateFlags; use rand::{Rng, SeedableRng}; use rand_pcg::Pcg32; use std::collections::HashMap; @@ -362,9 +363,11 @@ impl GraphicsManager { pub fn draw( &mut self, + flags: TestbedStateFlags, _bodies: &RigidBodySet, colliders: &ColliderSet, components: &mut Query<&mut Transform>, + visibilities: &mut Query<&mut Visibility>, _materials: &mut Assets, ) { for (_, ns) in self.b2sn.iter_mut() { @@ -386,6 +389,14 @@ impl GraphicsManager { // } // } + if let Ok(mut vis) = visibilities.get_mut(n.entity) { + if flags.contains(TestbedStateFlags::DRAW_SURFACES) { + *vis = Visibility::Inherited; + } else { + *vis = Visibility::Hidden; + } + } + n.update(colliders, components, &self.gfx_shift); } } diff --git a/src_testbed/lib.rs b/src_testbed/lib.rs index 9ececee..1eec06f 100644 --- a/src_testbed/lib.rs +++ b/src_testbed/lib.rs @@ -23,6 +23,8 @@ pub mod physics; #[cfg(all(feature = "dim3", feature = "other-backends"))] mod physx_backend; mod plugin; +mod save; +mod settings; mod testbed; mod ui; diff --git a/src_testbed/save.rs b/src_testbed/save.rs new file mode 100644 index 0000000..825226e --- /dev/null +++ b/src_testbed/save.rs @@ -0,0 +1,34 @@ +#[cfg(feature = "dim2")] +use crate::camera2d::OrbitCamera; +#[cfg(feature = "dim3")] +use crate::camera3d::OrbitCamera; +use crate::settings::ExampleSettings; +use crate::testbed::{RapierSolverType, RunMode, TestbedStateFlags}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)] +pub struct SerializableTestbedState { + pub running: RunMode, + pub flags: TestbedStateFlags, + pub selected_example: usize, + pub selected_backend: usize, + pub example_settings: ExampleSettings, + pub solver_type: RapierSolverType, + pub physx_use_two_friction_directions: bool, + pub camera: OrbitCamera, +} + +#[cfg(feature = "dim2")] +#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)] +pub struct SerializableCameraState { + pub zoom: f32, + pub center: na::Point2, +} + +#[cfg(feature = "dim3")] +#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)] +pub struct SerializableCameraState { + pub distance: f32, + pub position: na::Point3, + pub center: na::Point3, +} diff --git a/src_testbed/settings.rs b/src_testbed/settings.rs new file mode 100644 index 0000000..1e26a0f --- /dev/null +++ b/src_testbed/settings.rs @@ -0,0 +1,106 @@ +use std::collections::HashMap; +use std::ops::RangeInclusive; + +#[derive(Clone, PartialEq, Debug, serde::Serialize, serde::Deserialize)] +pub enum SettingValue { + U32 { + value: u32, + range: RangeInclusive, + }, + F32 { + value: f32, + range: RangeInclusive, + }, +} + +#[derive(Default, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct ExampleSettings { + values: HashMap, +} + +impl ExampleSettings { + pub fn clear(&mut self) { + self.values.clear(); + } + + pub fn len(&self) -> usize { + self.values.len() + } + + pub fn is_empty(&self) -> bool { + self.values.is_empty() + } + + pub fn iter_mut(&mut self) -> impl Iterator { + self.values.iter_mut() + } + + pub fn set_u32(&mut self, key: &str, value: u32, range: RangeInclusive) { + self.values + .insert(key.to_string(), SettingValue::U32 { value, range }); + } + + pub fn get_or_set_u32( + &mut self, + key: &'static str, + default: u32, + range: RangeInclusive, + ) -> u32 { + let to_insert = SettingValue::U32 { + value: default, + range, + }; + let entry = self + .values + .entry(key.to_string()) + .or_insert(to_insert.clone()); + match entry { + SettingValue::U32 { value, .. } => *value, + _ => { + // The entry doesn’t have the right type. Overwrite with the new value. + *entry = to_insert; + default + } + } + } + + pub fn set_f32(&mut self, key: &str, value: f32, range: RangeInclusive) { + self.values + .insert(key.to_string(), SettingValue::F32 { value, range }); + } + + pub fn get_or_set_f32( + &mut self, + key: &'static str, + value: f32, + range: RangeInclusive, + ) -> f32 { + let to_insert = SettingValue::F32 { value, range }; + let entry = self + .values + .entry(key.to_string()) + .or_insert(to_insert.clone()); + match entry { + SettingValue::F32 { value, .. } => *value, + _ => { + // The entry doesn’t have the right type. Overwrite with the new value. + *entry = to_insert; + value + } + } + } + + pub fn get_u32(&self, key: &'static str) -> Option { + match self.values.get(key)? { + SettingValue::U32 { value, .. } => Some(*value), + _ => None, + } + } + + pub fn get_f32(&self, key: &'static str) -> Option { + match self.values.get(key)? { + SettingValue::F32 { value, .. } => Some(*value), + _ => None, + } + } +} diff --git a/src_testbed/testbed.rs b/src_testbed/testbed.rs index 37b5710..ce60235 100644 --- a/src_testbed/testbed.rs +++ b/src_testbed/testbed.rs @@ -1,12 +1,11 @@ #![allow(clippy::bad_bit_mask)] // otherwise clippy complains because of TestbedStateFlags::NONE which is 0. #![allow(clippy::unnecessary_cast)] // allowed for f32 -> f64 cast for the f64 testbed. +use bevy::prelude::*; use std::env; use std::mem; use std::num::NonZeroUsize; -use bevy::prelude::*; - use crate::debug_render::{DebugRenderPipelineResource, RapierDebugRenderPlugin}; use crate::graphics::BevyMaterialComponent; use crate::physics::{DeserializedPhysicsSnapshot, PhysicsEvents, PhysicsSnapshot, PhysicsState}; @@ -51,17 +50,21 @@ const BOX2D_BACKEND: usize = 1; pub(crate) const PHYSX_BACKEND_PATCH_FRICTION: usize = 1; pub(crate) const PHYSX_BACKEND_TWO_FRICTION_DIR: usize = 2; -#[derive(PartialEq)] +pub fn save_file_path() -> String { + format!("testbed_state_{}.autosave.json", env!("CARGO_CRATE_NAME")) +} + +#[derive(Default, PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum RunMode { Running, + #[default] Stop, Step, } bitflags::bitflags! { - #[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] + #[derive(Copy, Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)] pub struct TestbedStateFlags: u32 { - const NONE = 0; const SLEEP = 1 << 0; const SUB_STEPPING = 1 << 1; const SHAPES = 1 << 2; @@ -72,6 +75,13 @@ bitflags::bitflags! { const CENTER_OF_MASSES = 1 << 7; const WIREFRAME = 1 << 8; const STATISTICS = 1 << 9; + const DRAW_SURFACES = 1 << 10; + } +} + +impl Default for TestbedStateFlags { + fn default() -> Self { + TestbedStateFlags::DRAW_SURFACES | TestbedStateFlags::SLEEP } } @@ -84,10 +94,11 @@ bitflags::bitflags! { const BACKEND_CHANGED = 1 << 3; const TAKE_SNAPSHOT = 1 << 4; const RESTORE_SNAPSHOT = 1 << 5; + const APP_STARTED = 1 << 6; } } -#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)] pub enum RapierSolverType { #[default] TgsSoft, @@ -118,13 +129,42 @@ pub struct TestbedState { pub example_names: Vec<&'static str>, pub selected_example: usize, pub selected_backend: usize, + pub example_settings: ExampleSettings, pub solver_type: RapierSolverType, pub physx_use_two_friction_directions: bool, pub snapshot: Option, pub nsteps: usize, + prev_save_data: SerializableTestbedState, camera_locked: bool, // Used so that the camera can remain the same before and after we change backend or press the restart button. } +impl TestbedState { + fn save_data(&self, camera: OrbitCamera) -> SerializableTestbedState { + SerializableTestbedState { + running: self.running, + flags: self.flags, + selected_example: self.selected_example, + selected_backend: self.selected_backend, + example_settings: self.example_settings.clone(), + solver_type: self.solver_type, + physx_use_two_friction_directions: self.physx_use_two_friction_directions, + camera, + } + } + + pub fn apply_saved_data(&mut self, state: SerializableTestbedState, camera: &mut OrbitCamera) { + self.prev_save_data = state.clone(); + self.running = state.running; + self.flags = state.flags; + self.selected_example = state.selected_example; + self.selected_backend = state.selected_backend; + self.example_settings = state.example_settings; + self.solver_type = state.solver_type; + self.physx_use_two_friction_directions = state.physx_use_two_friction_directions; + *camera = state.camera; + } +} + #[derive(Resource)] struct SceneBuilders(SimulationBuilders); @@ -172,7 +212,7 @@ pub struct TestbedApp { impl TestbedApp { pub fn new_empty() -> Self { let graphics = GraphicsManager::new(); - let flags = TestbedStateFlags::SLEEP; + let flags = TestbedStateFlags::default(); #[allow(unused_mut)] let mut backend_names = vec!["rapier"]; @@ -199,15 +239,17 @@ impl TestbedApp { snapshot: None, prev_flags: flags, flags, - action_flags: TestbedActionFlags::empty(), + action_flags: TestbedActionFlags::APP_STARTED | TestbedActionFlags::EXAMPLE_CHANGED, backend_names, example_names: Vec::new(), + example_settings: ExampleSettings::default(), selected_example: 0, selected_backend: RAPIER_BACKEND, solver_type: RapierSolverType::default(), physx_use_two_friction_directions: true, nsteps: 1, camera_locked: false, + prev_save_data: SerializableTestbedState::default(), }; let harness = Harness::new_empty(); @@ -230,12 +272,8 @@ impl TestbedApp { } } - pub fn from_builders(default: usize, builders: SimulationBuilders) -> Self { + pub fn from_builders(builders: SimulationBuilders) -> Self { let mut res = TestbedApp::new_empty(); - res.state - .action_flags - .set(TestbedActionFlags::EXAMPLE_CHANGED, true); - res.state.selected_example = default; res.set_builders(builders); res } @@ -566,6 +604,10 @@ impl Testbed<'_, '_, '_, '_, '_, '_> { self.harness } + pub fn example_settings_mut(&mut self) -> &mut ExampleSettings { + &mut self.state.example_settings + } + pub fn set_world( &mut self, bodies: RigidBodySet, @@ -1171,6 +1213,8 @@ fn egui_focus(mut ui_context: EguiContexts, mut cameras: Query<&mut OrbitCamera> } use crate::mouse::{track_mouse_state, MainCamera, SceneMouse}; +use crate::save::SerializableTestbedState; +use crate::settings::ExampleSettings; use bevy::window::PrimaryWindow; #[allow(clippy::type_complexity)] @@ -1189,8 +1233,9 @@ fn update_testbed( #[cfg(feature = "other-backends")] mut other_backends: NonSendMut, mut plugins: NonSendMut, mut ui_context: EguiContexts, - (mut gfx_components, mut cameras, mut material_handles): ( + (mut gfx_components, mut visibilities, mut cameras, mut material_handles): ( Query<&mut Transform>, + Query<&mut Visibility>, Query<(&Camera, &GlobalTransform, &mut OrbitCamera)>, Query<&mut BevyMaterialComponent>, ), @@ -1278,6 +1323,24 @@ fn update_testbed( .set(TestbedActionFlags::EXAMPLE_CHANGED, true); } + #[cfg(not(target_arch = "wasm32"))] + { + let app_started = state.action_flags.contains(TestbedActionFlags::APP_STARTED); + + if app_started { + state + .action_flags + .set(TestbedActionFlags::APP_STARTED, false); + if let Some(saved_state) = std::fs::read(save_file_path()) + .ok() + .and_then(|data| serde_json::from_slice::(&data).ok()) + { + state.apply_saved_data(saved_state, &mut cameras.single_mut().2); + state.camera_locked = true; + } + } + } + let example_changed = state .action_flags .contains(TestbedActionFlags::EXAMPLE_CHANGED); @@ -1296,6 +1359,10 @@ fn update_testbed( let graphics = &mut *graphics; let meshes = &mut *meshes; + if !restarted { + state.example_settings.clear(); + } + let graphics_context = TestbedGraphics { graphics: &mut *graphics, commands: &mut commands, @@ -1543,10 +1610,13 @@ fn update_testbed( } }; + // Draw graphics.draw( + state.flags, &harness.physics.bodies, &harness.physics.colliders, &mut gfx_components, + &mut visibilities, &mut *materials, ); @@ -1568,6 +1638,20 @@ fn update_testbed( if state.running == RunMode::Step { state.running = RunMode::Stop; } + + // If any saveable settings changed, save them again. + #[cfg(not(target_arch = "wasm32"))] + { + let new_save_data = state.save_data(cameras.single().2.clone()); + if state.prev_save_data != new_save_data { + // Save the data in a file. + let data = serde_json::to_string_pretty(&new_save_data).unwrap(); + if let Err(e) = std::fs::write(save_file_path(), &data) { + error!("Failed to write autosave file: {}", e); + } + state.prev_save_data = new_save_data; + } + } } fn clear( diff --git a/src_testbed/ui.rs b/src_testbed/ui.rs index 5945c38..05fe1e2 100644 --- a/src_testbed/ui.rs +++ b/src_testbed/ui.rs @@ -10,6 +10,7 @@ use crate::testbed::{ PHYSX_BACKEND_PATCH_FRICTION, PHYSX_BACKEND_TWO_FRICTION_DIR, }; +use crate::settings::SettingValue; use crate::PhysicsState; use bevy_egui::egui::{Slider, Ui}; use bevy_egui::{egui, EguiContexts}; @@ -24,39 +25,11 @@ pub fn update_ui( ) { #[cfg(feature = "profiler_ui")] { - let window = egui::Window::new("Profiling"); - let window = window.default_open(false); - - #[cfg(feature = "unstable-puffin-pr-235")] - { - use std::sync::Once; - static START: Once = Once::new(); - - fn set_default_rapier_filter() { - let mut profile_ui = puffin_egui::PROFILE_UI.lock(); - profile_ui - .profiler_ui - .flamegraph_options - .scope_name_filter - .set_filter("Harness::step_with_graphics".to_string()); - } - START.call_once(|| { - set_default_rapier_filter(); - }); - window.show(ui_context.ctx_mut(), |ui| { - if ui.button("🔍 Rapier filter").clicked() { - set_default_rapier_filter(); - } - puffin_egui::profiler_ui(ui); - }); - } - - #[cfg(not(feature = "unstable-puffin-pr-235"))] - window.show(ui_context.ctx_mut(), |ui| { - puffin_egui::profiler_ui(ui); - }); + profiler_ui(ui_context); } + example_settings_ui(ui_context, state); + egui::Window::new("Parameters").show(ui_context.ctx_mut(), |ui| { if state.backend_names.len() > 1 && !state.example_names.is_empty() { let mut changed = false; @@ -264,14 +237,17 @@ pub fn update_ui( integration_parameters.set_inv_dt(frequency as Real); let mut sleep = state.flags.contains(TestbedStateFlags::SLEEP); + let mut draw_surfaces = state.flags.contains(TestbedStateFlags::DRAW_SURFACES); // let mut contact_points = state.flags.contains(TestbedStateFlags::CONTACT_POINTS); // let mut wireframe = state.flags.contains(TestbedStateFlags::WIREFRAME); ui.checkbox(&mut sleep, "sleep enabled"); // ui.checkbox(&mut contact_points, "draw contacts"); // ui.checkbox(&mut wireframe, "draw wireframes"); + ui.checkbox(&mut draw_surfaces, "surface render enabled"); ui.checkbox(&mut debug_render.enabled, "debug render enabled"); state.flags.set(TestbedStateFlags::SLEEP, sleep); + state.flags.set(TestbedStateFlags::DRAW_SURFACES, draw_surfaces); // state // .flags // .set(TestbedStateFlags::CONTACT_POINTS, contact_points); @@ -481,3 +457,77 @@ Hashes at frame: {} format!("{:?}", hash_joints).split_at(10).0, ) } + +fn example_settings_ui(ui_context: &mut EguiContexts, state: &mut TestbedState) { + if state.example_settings.is_empty() { + // Don’t show any window if there is no settings for the + // example. + return; + } + + egui::Window::new("Example settings").show(ui_context.ctx_mut(), |ui| { + let mut any_changed = false; + for (name, value) in state.example_settings.iter_mut() { + let prev_value = value.clone(); + match value { + SettingValue::F32 { value, range } => { + ui.add(Slider::new(value, range.clone()).text(name)); + } + SettingValue::U32 { value, range } => { + ui.horizontal(|ui| { + if ui.button("<").clicked() && *value > *range.start() { + *value -= 1; + } + if ui.button(">").clicked() && *value <= *range.end() { + *value += 1; + } + + ui.add(Slider::new(value, range.clone()).text(name)); + }); + } + } + + any_changed = any_changed || *value != prev_value; + } + + if any_changed { + // The value changed, request a restart. + state.action_flags.set(TestbedActionFlags::RESTART, true); + } + }); +} + +#[cfg(feature = "profiler_ui")] +fn profiler_ui(ui_context: &mut EguiContexts) { + let window = egui::Window::new("Profiling"); + let window = window.default_open(false); + + #[cfg(feature = "unstable-puffin-pr-235")] + { + use std::sync::Once; + static START: Once = Once::new(); + + fn set_default_rapier_filter() { + let mut profile_ui = puffin_egui::PROFILE_UI.lock(); + profile_ui + .profiler_ui + .flamegraph_options + .scope_name_filter + .set_filter("Harness::step_with_graphics".to_string()); + } + START.call_once(|| { + set_default_rapier_filter(); + }); + window.show(ui_context.ctx_mut(), |ui| { + if ui.button("🔍 Rapier filter").clicked() { + set_default_rapier_filter(); + } + puffin_egui::profiler_ui(ui); + }); + } + + #[cfg(not(feature = "unstable-puffin-pr-235"))] + window.show(ui_context.ctx_mut(), |ui| { + puffin_egui::profiler_ui(ui); + }); +}