feat(rapier_testbed): autosave testbed configuration + add support for per-example settings (#799)

* feat(rapier_testbed): autosave testbed configuration + add support for per-example parameters

* feat(rapier_testbed): also save the camera position

* feat(rapier_testbed): improve ergonomics of example-specific settings

* chore: cargo fmt

* chore(rapier_testbed): small UI ergonomics improvement

* chore(rapier_testbed): clippy fixes
This commit is contained in:
Sébastien Crozet
2025-02-21 17:52:46 +01:00
committed by GitHub
parent bf8e48e920
commit 5ca6ae9106
23 changed files with 358 additions and 195 deletions

View File

@@ -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<PhysicsSnapshot>,
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<OtherBackends>,
mut plugins: NonSendMut<Plugins>,
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::<SerializableTestbedState>(&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(