feat: migrate to glam whenever relevant + migrate testbed to kiss3d instead of bevy + release v0.32.0 (#909)

* feat: migrate to glam whenever relevant + migrate testbed to kiss3d instead of bevy

* chore: update changelog

* Fix warnings and tests

* Release v0.32.0
This commit is contained in:
Sébastien Crozet
2026-01-09 17:26:36 +01:00
committed by GitHub
parent 48de83817e
commit 0b7c3b34ec
265 changed files with 8501 additions and 8575 deletions

658
src_testbed/testbed/app.rs Normal file
View File

@@ -0,0 +1,658 @@
//! TestbedApp - the main application runner.
use crate::Camera;
use crate::debug_render::{DebugRenderPipelineResource, debug_render_scene};
use crate::graphics::GraphicsManager;
use crate::harness::Harness;
use crate::mouse::SceneMouse;
use crate::save::SerializableTestbedState;
use crate::testbed::hover::highlight_hovered_body;
use crate::ui;
use kiss3d::color::Color;
use kiss3d::event::{Action, Key, WindowEvent};
use kiss3d::window::Window;
use rapier::dynamics::RigidBodyActivation;
use std::mem;
use super::Plugins;
use super::graphics_context::TestbedGraphics;
use super::keys::KeysState;
use super::state::{RAPIER_BACKEND, RunMode, TestbedActionFlags, TestbedState, TestbedStateFlags};
use super::testbed::{SimulationBuilders, Testbed};
#[cfg(feature = "other-backends")]
use super::OtherBackends;
#[cfg(all(feature = "dim3", feature = "other-backends"))]
use super::state::{PHYSX_BACKEND_PATCH_FRICTION, PHYSX_BACKEND_TWO_FRICTION_DIR};
/// The main testbed application
pub struct TestbedApp {
builders: SimulationBuilders,
graphics: GraphicsManager,
state: TestbedState,
harness: Harness,
#[cfg(feature = "other-backends")]
other_backends: OtherBackends,
plugins: Plugins,
}
impl TestbedApp {
pub fn save_file_path() -> String {
format!("testbed_state_{}.autosave.json", env!("CARGO_CRATE_NAME"))
}
pub fn new_empty() -> Self {
let graphics = GraphicsManager::new();
let state = TestbedState::default();
let harness = Harness::new_empty();
#[cfg(feature = "other-backends")]
let other_backends = OtherBackends {
#[cfg(feature = "dim3")]
physx: None,
};
TestbedApp {
builders: Vec::new(),
plugins: Plugins(Vec::new()),
graphics,
state,
harness,
#[cfg(feature = "other-backends")]
other_backends,
}
}
pub fn from_builders(builders: SimulationBuilders) -> Self {
let mut res = TestbedApp::new_empty();
res.set_builders(builders);
res
}
pub fn set_builders(&mut self, builders: SimulationBuilders) {
use super::state::ExampleEntry;
use indexmap::IndexSet;
// Collect unique groups in order of first appearance
let mut groups: IndexSet<&'static str> = IndexSet::new();
for example in &builders {
groups.insert(example.group);
}
// Build the display order: group by group, preserving original order within each group
let mut examples = Vec::new();
for group in &groups {
for (builder_index, example) in builders.iter().enumerate() {
if example.group == *group {
examples.push(ExampleEntry {
name: example.name,
group: example.group,
builder_index,
});
}
}
}
self.state.example_groups = groups.into_iter().collect();
self.state.examples = examples;
self.builders = builders;
}
pub async fn run(self) {
self.run_with_init(|_| {}).await
}
pub async fn run_with_init(mut self, init: impl FnMut(&mut Testbed)) {
#[cfg(feature = "profiler_ui")]
profiling::puffin::set_scopes_on(true);
// Check for benchmark mode
let args: Vec<String> = std::env::args().collect();
if args.iter().any(|a| a == "--bench") {
self.run_benchmark();
return;
}
self.run_async(init).await
}
fn run_benchmark(&mut self) {
use std::fs::File;
use std::io::{BufWriter, Write};
let num_bench_iters = 1000u32;
let builders = mem::take(&mut self.builders);
let backend_names = self.state.backend_names.clone();
for builder in &builders {
let mut results = Vec::new();
println!("Running benchmark for {}", builder.name);
for (backend_id, backend) in backend_names.iter().enumerate() {
println!("|_ using backend {backend}");
self.state.selected_backend = backend_id;
self.harness = Harness::new_empty();
let mut testbed = Testbed {
graphics: None,
state: &mut self.state,
harness: &mut self.harness,
#[cfg(feature = "other-backends")]
other_backends: &mut self.other_backends,
plugins: &mut self.plugins,
};
(builder.builder)(&mut testbed);
let mut timings = Vec::new();
for k in 0..num_bench_iters {
if self.state.selected_backend == RAPIER_BACKEND {
self.harness.step();
}
#[cfg(all(feature = "dim3", feature = "other-backends"))]
{
if self.state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION
|| self.state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR
{
self.other_backends.physx.as_mut().unwrap().step(
&mut self.harness.physics.pipeline.counters,
&self.harness.physics.integration_parameters,
);
self.other_backends.physx.as_mut().unwrap().sync(
&mut self.harness.physics.bodies,
&mut self.harness.physics.colliders,
);
}
}
if k > 0 {
timings.push(self.harness.physics.pipeline.counters.step_time.time_ms());
}
}
results.push(timings);
}
use inflector::Inflector;
let filename = format!("{}.csv", builder.name.to_camel_case());
let mut file = BufWriter::new(File::create(filename).unwrap());
write!(file, "{}", backend_names[0]).unwrap();
for backend in &backend_names[1..] {
write!(file, ",{backend}").unwrap();
}
writeln!(file).unwrap();
for i in 0..results[0].len() {
write!(file, "{}", results[0][i]).unwrap();
for result in &results[1..] {
write!(file, ",{}", result[i]).unwrap();
}
writeln!(file).unwrap();
}
}
}
async fn run_async(mut self, mut init: impl FnMut(&mut Testbed)) {
let title = if cfg!(feature = "dim2") {
"Rapier: 2D demos"
} else {
"Rapier: 3D demos"
};
let mut window = Window::new_with_size(title, 1280, 720).await;
window.set_background_color(Color::new(245.0 / 255.0, 245.0 / 255.0, 236.0 / 255.0, 1.0));
let mut debug_render = DebugRenderPipelineResource::default();
let mut camera = Camera::default();
let mut scene_mouse = SceneMouse::new();
let mut keys = KeysState::default();
// User init
let testbed_gfx = TestbedGraphics {
graphics: &mut self.graphics,
window: &mut window,
camera: &mut camera,
mouse: &mut scene_mouse,
keys: &mut keys,
settings: None,
};
let mut testbed = Testbed {
graphics: Some(testbed_gfx),
state: &mut self.state,
harness: &mut self.harness,
#[cfg(feature = "other-backends")]
other_backends: &mut self.other_backends,
plugins: &mut self.plugins,
};
init(&mut testbed);
// Main render loop
#[cfg(feature = "dim3")]
while window
.render_3d(self.graphics.scene_mut(), &mut camera)
.await
{
self.run_frame(
&mut window,
&mut debug_render,
&mut camera,
&mut scene_mouse,
&mut keys,
);
}
#[cfg(feature = "dim2")]
while window
.render_2d(self.graphics.scene_mut(), &mut camera)
.await
{
self.run_frame(
&mut window,
&mut debug_render,
&mut camera,
&mut scene_mouse,
&mut keys,
);
}
}
fn run_frame(
&mut self,
window: &mut Window,
debug_render: &mut DebugRenderPipelineResource,
camera: &mut Camera,
scene_mouse: &mut SceneMouse,
keys: &mut KeysState,
) {
profiling::finish_frame!();
// Handle input events
self.handle_events(window, keys);
// Handle the vehicle controller if there is one.
#[cfg(feature = "dim3")]
{
self.update_vehicle_controller(keys);
}
// Update mouse state
let cursor_pos = window.cursor_pos();
scene_mouse.update_from_window(cursor_pos, window.size().into(), camera);
// Handle action flags
self.handle_action_flags(window, camera, scene_mouse, keys);
// Handle sleep settings
self.handle_sleep_settings();
// Run simulation
if self.state.running != RunMode::Stop {
for _ in 0..self.state.nsteps {
if self.state.selected_backend == RAPIER_BACKEND {
let mut testbed_gfx = TestbedGraphics {
graphics: &mut self.graphics,
window,
camera,
mouse: scene_mouse,
keys,
settings: Some(&mut self.state.example_settings),
};
self.harness.step_with_graphics(Some(&mut testbed_gfx));
for plugin in &mut self.plugins.0 {
plugin.step(&mut self.harness.physics);
}
}
#[cfg(all(feature = "dim3", feature = "other-backends"))]
{
if self.state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION
|| self.state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR
{
self.other_backends.physx.as_mut().unwrap().step(
&mut self.harness.physics.pipeline.counters,
&self.harness.physics.integration_parameters,
);
self.other_backends.physx.as_mut().unwrap().sync(
&mut self.harness.physics.bodies,
&mut self.harness.physics.colliders,
);
}
}
for plugin in &mut self.plugins.0 {
plugin.run_callbacks(&mut self.harness);
}
}
if self.state.running == RunMode::Step {
self.state.running = RunMode::Stop;
}
}
// Autosave state.
#[cfg(not(target_arch = "wasm32"))]
{
let new_save_data = self.state.save_data(*camera);
if self.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(Self::save_file_path(), &data) {
eprintln!("Failed to write autosave file: {}", e);
}
self.state.prev_save_data = new_save_data;
}
}
highlight_hovered_body(&mut self.graphics, scene_mouse, &self.harness.physics);
// Update graphics
self.graphics.draw(
self.state.flags,
&self.harness.physics.bodies,
&self.harness.physics.colliders,
);
// Draw debug render
debug_render_scene(window, debug_render, &self.harness);
// Draw UI
window.draw_ui(|ctx| {
ui::update_ui(ctx, &mut self.state, &mut self.harness, debug_render);
});
self.state.prev_flags = self.state.flags;
}
fn handle_events(&mut self, window: &mut Window, keys: &mut KeysState) {
for event in window.events().iter() {
match event.value {
WindowEvent::Key(key, Action::Press, _) => {
// Track pressed keys
if !keys.pressed_keys.contains(&key) {
keys.pressed_keys.push(key);
}
// Update modifier states
match key {
Key::LShift | Key::RShift => keys.shift = true,
Key::LControl | Key::RControl => keys.ctrl = true,
Key::LAlt | Key::RAlt => keys.alt = true,
_ => {}
}
}
WindowEvent::Key(key, Action::Release, _) => {
// Remove from pressed keys
keys.pressed_keys.retain(|k| *k != key);
// Handle special keys
match key {
Key::T => {
if self.state.running == RunMode::Stop {
self.state.running = RunMode::Running;
} else {
self.state.running = RunMode::Stop;
}
}
Key::S => {
self.state.running = RunMode::Step;
}
Key::R => {
self.state
.action_flags
.set(TestbedActionFlags::EXAMPLE_CHANGED, true);
}
Key::LShift | Key::RShift => keys.shift = false,
Key::LControl | Key::RControl => keys.ctrl = false,
Key::LAlt | Key::RAlt => keys.alt = false,
_ => {}
}
}
_ => {}
}
}
}
#[cfg(feature = "dim3")]
fn update_vehicle_controller(&mut self, keys: &mut KeysState) {
use rapier::prelude::QueryFilter;
if self.state.running == RunMode::Stop {
return;
}
if let Some(vehicle) = &mut self.state.vehicle_controller {
let mut engine_force = 0.0;
let mut steering_angle = 0.0;
println!("Pressed: {:?}", keys);
if keys.pressed(Key::Right) {
steering_angle += -0.7;
}
if keys.pressed(Key::Left) {
steering_angle += 0.7;
}
if keys.pressed(Key::Up) {
engine_force += 30.0;
}
if keys.pressed(Key::Down) {
engine_force += -30.0;
}
let wheels = vehicle.wheels_mut();
wheels[0].engine_force = engine_force;
wheels[0].steering = steering_angle;
wheels[1].engine_force = engine_force;
wheels[1].steering = steering_angle;
let query_pipeline = self.harness.physics.broad_phase.as_query_pipeline_mut(
self.harness.physics.narrow_phase.query_dispatcher(),
&mut self.harness.physics.bodies,
&mut self.harness.physics.colliders,
QueryFilter::exclude_dynamic().exclude_rigid_body(vehicle.chassis),
);
vehicle.update_vehicle(
self.harness.physics.integration_parameters.dt,
query_pipeline,
);
}
}
fn handle_action_flags(
&mut self,
window: &mut Window,
camera: &mut Camera,
scene_mouse: &mut SceneMouse,
keys: &mut KeysState,
) {
#[cfg(not(target_arch = "wasm32"))]
{
let app_started = self
.state
.action_flags
.contains(TestbedActionFlags::APP_STARTED);
if app_started {
self.state
.action_flags
.set(TestbedActionFlags::APP_STARTED, false);
if let Some(saved_state) = std::fs::read(Self::save_file_path())
.ok()
.and_then(|data| serde_json::from_slice::<SerializableTestbedState>(&data).ok())
{
self.state.apply_saved_data(saved_state, camera);
self.state.camera_locked = true;
}
}
}
let backend_changed = self
.state
.action_flags
.contains(TestbedActionFlags::BACKEND_CHANGED);
if backend_changed {
self.state
.action_flags
.set(TestbedActionFlags::BACKEND_CHANGED, false);
self.state
.action_flags
.set(TestbedActionFlags::EXAMPLE_CHANGED, true);
self.state.camera_locked = true;
}
let restarted = self
.state
.action_flags
.contains(TestbedActionFlags::RESTART);
if restarted {
self.state
.action_flags
.set(TestbedActionFlags::RESTART, false);
self.state.camera_locked = true;
self.state
.action_flags
.set(TestbedActionFlags::EXAMPLE_CHANGED, true);
}
let example_changed = self
.state
.action_flags
.contains(TestbedActionFlags::EXAMPLE_CHANGED);
if example_changed {
self.state
.action_flags
.set(TestbedActionFlags::EXAMPLE_CHANGED, false);
self.clear(window);
self.harness.clear_callbacks();
if !self.state.camera_locked {
*camera = Camera::default();
}
if !restarted {
self.state.example_settings.clear();
}
// Clamp selected_display_index to valid range
let max_index = self.state.examples.len().saturating_sub(1);
self.state.selected_display_index = self.state.selected_display_index.min(max_index);
if !self.builders.is_empty() {
let builder_index = self.state.selected_builder_index();
let builder = self.builders[builder_index].builder;
let testbed_gfx = TestbedGraphics {
graphics: &mut self.graphics,
window,
camera,
mouse: scene_mouse,
keys,
settings: None,
};
let mut testbed = Testbed {
graphics: Some(testbed_gfx),
state: &mut self.state,
harness: &mut self.harness,
#[cfg(feature = "other-backends")]
other_backends: &mut self.other_backends,
plugins: &mut self.plugins,
};
builder(&mut testbed);
}
self.state.camera_locked = false;
}
if self
.state
.action_flags
.contains(TestbedActionFlags::RESET_WORLD_GRAPHICS)
{
self.state
.action_flags
.set(TestbedActionFlags::RESET_WORLD_GRAPHICS, false);
for (handle, _) in self.harness.physics.bodies.iter() {
self.graphics.add_body_colliders(
window,
handle,
&self.harness.physics.bodies,
&self.harness.physics.colliders,
);
}
for (handle, co) in self.harness.physics.colliders.iter() {
if co.parent().is_none() {
self.graphics
.add_collider(window, handle, &self.harness.physics.colliders);
}
}
}
if self
.state
.action_flags
.contains(TestbedActionFlags::TAKE_SNAPSHOT)
{
self.state
.action_flags
.set(TestbedActionFlags::TAKE_SNAPSHOT, false);
self.state.snapshot = Some(self.harness.physics.snapshot());
}
if self
.state
.action_flags
.contains(TestbedActionFlags::RESTORE_SNAPSHOT)
{
self.state
.action_flags
.set(TestbedActionFlags::RESTORE_SNAPSHOT, false);
if let Some(snapshot) = &self.state.snapshot {
self.harness.physics.restore_snapshot(snapshot.clone());
self.state
.action_flags
.set(TestbedActionFlags::RESET_WORLD_GRAPHICS, true);
}
}
if example_changed
|| self.state.prev_flags.contains(TestbedStateFlags::WIREFRAME)
!= self.state.flags.contains(TestbedStateFlags::WIREFRAME)
{
self.graphics.toggle_wireframe_mode(
&self.harness.physics.colliders,
self.state.flags.contains(TestbedStateFlags::WIREFRAME),
);
}
}
fn handle_sleep_settings(&mut self) {
if self.state.prev_flags.contains(TestbedStateFlags::SLEEP)
!= self.state.flags.contains(TestbedStateFlags::SLEEP)
{
if self.state.flags.contains(TestbedStateFlags::SLEEP) {
for (_, body) in self.harness.physics.bodies.iter_mut() {
body.activation_mut().normalized_linear_threshold =
RigidBodyActivation::default_normalized_linear_threshold();
body.activation_mut().angular_threshold =
RigidBodyActivation::default_angular_threshold();
}
} else {
for (_, body) in self.harness.physics.bodies.iter_mut() {
body.wake_up(true);
body.activation_mut().normalized_linear_threshold = -1.0;
}
}
}
}
fn clear(&mut self, window: &mut Window) {
self.state.can_grab_behind_ground = false;
self.graphics.clear();
for mut plugin in self.plugins.0.drain(..) {
plugin.clear_graphics(&mut self.graphics, window);
}
}
}

View File

@@ -0,0 +1,94 @@
//! Graphics context for a frame.
use kiss3d::color::Color;
use kiss3d::window::Window;
use rapier::dynamics::RigidBodyHandle;
use rapier::dynamics::RigidBodySet;
use rapier::geometry::{ColliderHandle, ColliderSet};
use crate::Camera;
use crate::graphics::GraphicsManager;
use crate::mouse::SceneMouse;
use crate::settings::ExampleSettings;
use super::keys::KeysState;
/// Context for graphics operations during a frame
pub struct TestbedGraphics<'a> {
pub graphics: &'a mut GraphicsManager,
pub window: &'a mut Window,
pub camera: &'a mut Camera,
pub mouse: &'a SceneMouse,
pub keys: &'a KeysState,
pub settings: Option<&'a mut ExampleSettings>,
}
impl<'a> TestbedGraphics<'a> {
pub fn set_body_color(&mut self, body: RigidBodyHandle, color: Color, tmp_color: bool) {
self.graphics.set_body_color(body, color, tmp_color);
}
pub fn add_body(
&mut self,
handle: RigidBodyHandle,
bodies: &RigidBodySet,
colliders: &ColliderSet,
) {
self.graphics
.add_body_colliders(self.window, handle, bodies, colliders);
}
pub fn remove_body(&mut self, handle: RigidBodyHandle) {
self.graphics.remove_body_nodes(handle);
}
pub fn add_collider(&mut self, handle: ColliderHandle, colliders: &ColliderSet) {
self.graphics.add_collider(self.window, handle, colliders);
}
pub fn remove_collider(&mut self, handle: ColliderHandle) {
self.graphics.remove_collider_nodes(handle);
}
pub fn keys(&self) -> &KeysState {
self.keys
}
pub fn mouse(&self) -> &SceneMouse {
self.mouse
}
/// Update collider graphics after shape modification
pub fn update_collider(&mut self, handle: ColliderHandle, colliders: &ColliderSet) {
// Remove and re-add the collider to update its graphics
self.graphics.remove_collider_nodes(handle);
self.graphics.add_collider(self.window, handle, colliders);
}
/// Get the camera rotation as a unit quaternion (3D only)
#[cfg(feature = "dim3")]
pub fn camera_rotation(&self) -> na::UnitQuaternion<f32> {
// Calculate rotation from orbit camera angles
let rot_x = na::UnitQuaternion::from_axis_angle(&na::Vector3::y_axis(), self.camera.at().x);
let rot_y =
na::UnitQuaternion::from_axis_angle(&(-na::Vector3::x_axis()), self.camera.at().y);
rot_x * rot_y
}
/// Get the camera forward direction (3D only)
#[cfg(feature = "dim3")]
pub fn camera_fwd_dir(&self) -> na::Vector3<f32> {
let rot = self.camera_rotation();
rot * na::Vector3::z()
}
/// Get mutable access to the egui context for custom UI
pub fn egui_context(&self) -> &egui::Context {
self.window.egui_context()
}
/// Get mutable access to the egui context for custom UI
pub fn egui_context_mut(&mut self) -> &mut egui::Context {
self.window.egui_context_mut()
}
}

View File

@@ -0,0 +1,64 @@
#![allow(clippy::useless_conversion)] // Conversions are needed for switching between f32/f64.
use crate::mouse::SceneMouse;
use crate::{GraphicsManager, PhysicsState};
use kiss3d::prelude::*;
use rapier::prelude::QueryFilter;
#[cfg(feature = "dim3")]
use rapier::prelude::{Ray, Real};
#[cfg(feature = "dim2")]
pub fn highlight_hovered_body(
graphics_manager: &mut GraphicsManager,
mouse: &SceneMouse,
physics: &PhysicsState,
) {
use rapier::math::Vector;
if let Some(pt) = mouse.point {
// Convert from kiss3d Vec2 (f32) to rapier Vector (may be f64)
let pt = Vector::new(pt.x as _, pt.y as _);
let query_pipeline = physics.broad_phase.as_query_pipeline(
physics.narrow_phase.query_dispatcher(),
&physics.bodies,
&physics.colliders,
QueryFilter::only_dynamic(),
);
for (handle, _) in query_pipeline.intersect_point(pt) {
let collider = &physics.colliders[handle];
if let Some(parent_handle) = collider.parent() {
graphics_manager.set_body_color(parent_handle, RED, true);
}
}
}
}
#[cfg(feature = "dim3")]
pub fn highlight_hovered_body(
graphics_manager: &mut GraphicsManager,
mouse: &SceneMouse,
physics: &PhysicsState,
) {
if let Some((ray_origin, ray_dir)) = mouse.ray {
let ray = Ray::new(ray_origin.into(), ray_dir.into());
let query_pipeline = physics.broad_phase.as_query_pipeline(
physics.narrow_phase.query_dispatcher(),
&physics.bodies,
&physics.colliders,
QueryFilter::only_dynamic(),
);
let hit = query_pipeline.cast_ray(&ray, Real::MAX, true);
if let Some((handle, _)) = hit {
let collider = &physics.colliders[handle];
if let Some(parent_handle) = collider.parent() {
graphics_manager.set_body_color(parent_handle, RED, true);
}
}
}
}

View File

@@ -0,0 +1,24 @@
//! Keyboard state tracking.
use kiss3d::event::Key;
/// Keyboard state
#[derive(Default, Clone, Debug)]
pub struct KeysState {
pub shift: bool,
pub ctrl: bool,
pub alt: bool,
pub pressed_keys: Vec<Key>,
}
impl KeysState {
/// Check if a specific key is currently pressed
pub fn pressed(&self, key: Key) -> bool {
self.pressed_keys.contains(&key)
}
/// Get all currently pressed keys
pub fn get_pressed(&self) -> &[Key] {
&self.pressed_keys
}
}

View File

@@ -0,0 +1,35 @@
//! Testbed module - visual debugging and example runner for Rapier physics.
#![allow(clippy::bad_bit_mask)]
#![allow(clippy::unnecessary_cast)]
#![allow(clippy::module_inception)]
mod app;
mod graphics_context;
mod hover;
mod keys;
mod state;
mod testbed;
#[cfg(all(feature = "dim3", feature = "other-backends"))]
use crate::physx_backend::PhysxWorld;
// Re-export all public types
pub use app::TestbedApp;
pub use graphics_context::TestbedGraphics;
pub use keys::KeysState;
pub use state::{RunMode, TestbedActionFlags, TestbedState, TestbedStateFlags, UiTab};
pub use testbed::{Example, Testbed};
// Internal re-exports for other modules
pub(crate) use state::{PHYSX_BACKEND_PATCH_FRICTION, PHYSX_BACKEND_TWO_FRICTION_DIR};
/// Backend implementations for other physics engines
#[cfg(feature = "other-backends")]
pub struct OtherBackends {
#[cfg(feature = "dim3")]
pub physx: Option<PhysxWorld>,
}
/// Container for testbed plugins
pub struct Plugins(pub Vec<Box<dyn crate::plugin::TestbedPlugin>>);

View File

@@ -0,0 +1,159 @@
//! Testbed state types and flags.
use bitflags::bitflags;
use na::Point3;
#[cfg(feature = "dim3")]
use rapier::control::DynamicRayCastVehicleController;
use crate::harness::RapierBroadPhaseType;
use crate::physics::PhysicsSnapshot;
use crate::save::SerializableTestbedState;
use crate::settings::ExampleSettings;
/// Run mode for the simulation
#[derive(Default, PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum RunMode {
Running,
#[default]
Stop,
Step,
}
bitflags! {
/// Flags for controlling what is displayed in the testbed
#[derive(Copy, Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct TestbedStateFlags: u32 {
const SLEEP = 1 << 0;
const SUB_STEPPING = 1 << 1;
const SHAPES = 1 << 2;
const JOINTS = 1 << 3;
const AABBS = 1 << 4;
const CONTACT_POINTS = 1 << 5;
const CONTACT_NORMALS = 1 << 6;
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
}
}
bitflags! {
/// Flags for testbed actions that need to be processed
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct TestbedActionFlags: u32 {
const RESET_WORLD_GRAPHICS = 1 << 0;
const EXAMPLE_CHANGED = 1 << 1;
const RESTART = 1 << 2;
const BACKEND_CHANGED = 1 << 3;
const TAKE_SNAPSHOT = 1 << 4;
const RESTORE_SNAPSHOT = 1 << 5;
const APP_STARTED = 1 << 6;
}
}
pub(crate) const RAPIER_BACKEND: usize = 0;
pub(crate) const PHYSX_BACKEND_PATCH_FRICTION: usize = 1;
pub(crate) const PHYSX_BACKEND_TWO_FRICTION_DIR: usize = 2;
/// Which tab is currently selected in the UI
#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
pub enum UiTab {
#[default]
Examples,
Settings,
Performance,
}
/// Information about an example for UI display
#[derive(Clone, Debug)]
pub struct ExampleEntry {
pub name: &'static str,
pub group: &'static str,
/// Index in the original builders array
pub builder_index: usize,
}
/// State for the testbed application
pub struct TestbedState {
pub running: RunMode,
pub draw_colls: bool,
#[cfg(feature = "dim3")]
pub vehicle_controller: Option<DynamicRayCastVehicleController>,
pub grabbed_object_plane: (Point3<f32>, na::Vector3<f32>),
pub can_grab_behind_ground: bool,
pub drawing_ray: Option<na::Point2<f32>>,
pub prev_flags: TestbedStateFlags,
pub flags: TestbedStateFlags,
pub action_flags: TestbedActionFlags,
pub backend_names: Vec<&'static str>,
/// Examples in display order (grouped, then by original order within group)
pub examples: Vec<ExampleEntry>,
/// Unique group names in order of first appearance
pub example_groups: Vec<&'static str>,
/// Currently selected position in the display order
pub selected_display_index: usize,
pub selected_backend: usize,
pub example_settings: ExampleSettings,
pub broad_phase_type: RapierBroadPhaseType,
pub physx_use_two_friction_directions: bool,
pub snapshot: Option<PhysicsSnapshot>,
pub nsteps: usize,
pub camera_locked: bool,
pub selected_tab: UiTab,
pub prev_save_data: SerializableTestbedState,
}
impl Default for TestbedState {
fn default() -> Self {
#[allow(unused_mut)]
let mut backend_names = vec!["rapier"];
#[cfg(all(feature = "dim3", feature = "other-backends"))]
backend_names.push("physx (patch friction)");
#[cfg(all(feature = "dim3", feature = "other-backends"))]
backend_names.push("physx (two friction dir)");
let flags = TestbedStateFlags::default();
Self {
running: RunMode::Running,
draw_colls: false,
#[cfg(feature = "dim3")]
vehicle_controller: None,
grabbed_object_plane: (Point3::origin(), na::zero()),
can_grab_behind_ground: false,
drawing_ray: None,
snapshot: None,
prev_flags: flags,
flags,
action_flags: TestbedActionFlags::APP_STARTED | TestbedActionFlags::EXAMPLE_CHANGED,
backend_names,
examples: Vec::new(),
example_groups: Vec::new(),
example_settings: ExampleSettings::default(),
selected_display_index: 0,
selected_backend: RAPIER_BACKEND,
broad_phase_type: RapierBroadPhaseType::default(),
physx_use_two_friction_directions: true,
nsteps: 1,
camera_locked: false,
selected_tab: UiTab::default(),
prev_save_data: SerializableTestbedState::default(),
}
}
}
impl TestbedState {
/// Get the builder index for the currently selected example
pub fn selected_builder_index(&self) -> usize {
self.examples
.get(self.selected_display_index)
.map(|e| e.builder_index)
.unwrap_or(0)
}
}

View File

@@ -0,0 +1,243 @@
//! Testbed struct for building examples.
use kiss3d::color::Color;
use rapier::dynamics::{
ImpulseJointSet, IntegrationParameters, MultibodyJointSet, RigidBodyHandle, RigidBodySet,
};
use rapier::geometry::{ColliderHandle, ColliderSet};
use rapier::pipeline::PhysicsHooks;
#[cfg(feature = "dim3")]
use {glamx::Vec3, rapier::control::DynamicRayCastVehicleController};
use crate::harness::Harness;
use crate::physics::PhysicsState;
use crate::settings::ExampleSettings;
use super::graphics_context::TestbedGraphics;
use super::state::{TestbedActionFlags, TestbedState};
#[cfg(all(feature = "dim3", feature = "other-backends"))]
use super::OtherBackends;
#[cfg(all(feature = "dim3", feature = "other-backends"))]
use super::state::{PHYSX_BACKEND_PATCH_FRICTION, PHYSX_BACKEND_TWO_FRICTION_DIR};
#[cfg(all(feature = "dim3", feature = "other-backends"))]
use crate::physx_backend::PhysxWorld;
use super::Plugins;
/// An example/demo that can be run in the testbed
#[derive(Clone)]
pub struct Example {
/// Display name of the example
pub name: &'static str,
/// Group/category for organizing in the UI (e.g., "Demos", "Joints", "Debug")
pub group: &'static str,
/// The builder function that initializes the example
pub builder: fn(&mut Testbed),
}
impl Example {
/// Create a new example with a group
pub fn new(group: &'static str, name: &'static str, builder: fn(&mut Testbed)) -> Self {
Self {
name,
group,
builder,
}
}
/// Create a new example in the default "Demos" group
pub fn demo(name: &'static str, builder: fn(&mut Testbed)) -> Self {
Self::new("Demos", name, builder)
}
}
/// Allow constructing Example from a tuple (group, name, builder) for convenience
impl From<(&'static str, &'static str, fn(&mut Testbed))> for Example {
fn from((group, name, builder): (&'static str, &'static str, fn(&mut Testbed))) -> Self {
Self::new(group, name, builder)
}
}
/// Type alias for simulation builder functions
pub type SimulationBuilders = Vec<Example>;
/// The main testbed struct passed to example builders
pub struct Testbed<'a> {
pub graphics: Option<TestbedGraphics<'a>>,
pub harness: &'a mut Harness,
pub state: &'a mut TestbedState,
#[cfg(all(feature = "dim3", feature = "other-backends"))]
pub other_backends: &'a mut OtherBackends,
pub plugins: &'a mut Plugins,
}
impl Testbed<'_> {
pub fn set_number_of_steps_per_frame(&mut self, nsteps: usize) {
self.state.nsteps = nsteps;
}
#[cfg(feature = "dim3")]
pub fn set_vehicle_controller(&mut self, controller: DynamicRayCastVehicleController) {
self.state.vehicle_controller = Some(controller);
}
pub fn allow_grabbing_behind_ground(&mut self, allow: bool) {
self.state.can_grab_behind_ground = allow;
}
pub fn integration_parameters_mut(&mut self) -> &mut IntegrationParameters {
&mut self.harness.physics.integration_parameters
}
pub fn physics_state_mut(&mut self) -> &mut PhysicsState {
&mut self.harness.physics
}
pub fn harness(&self) -> &Harness {
self.harness
}
pub fn harness_mut(&mut self) -> &mut Harness {
self.harness
}
pub fn example_settings_mut(&mut self) -> &mut ExampleSettings {
&mut self.state.example_settings
}
pub fn set_world(
&mut self,
bodies: RigidBodySet,
colliders: ColliderSet,
impulse_joints: ImpulseJointSet,
multibody_joints: MultibodyJointSet,
) {
self.set_world_with_params(
bodies,
colliders,
impulse_joints,
multibody_joints,
rapier::math::Vector::Y * -9.81,
(),
)
}
pub fn set_world_with_params(
&mut self,
bodies: RigidBodySet,
colliders: ColliderSet,
impulse_joints: ImpulseJointSet,
multibody_joints: MultibodyJointSet,
gravity: rapier::math::Vector,
hooks: impl PhysicsHooks + 'static,
) {
self.harness.set_world_with_params(
bodies,
colliders,
impulse_joints,
multibody_joints,
self.state.broad_phase_type,
gravity,
hooks,
);
self.state
.action_flags
.set(TestbedActionFlags::RESET_WORLD_GRAPHICS, true);
#[cfg(feature = "dim3")]
{
self.state.vehicle_controller = None;
}
#[cfg(all(feature = "dim3", feature = "other-backends"))]
{
if self.state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION
|| self.state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR
{
self.other_backends.physx = Some(PhysxWorld::from_rapier(
self.harness.physics.gravity,
&self.harness.physics.integration_parameters,
&self.harness.physics.bodies,
&self.harness.physics.colliders,
&self.harness.physics.impulse_joints,
&self.harness.physics.multibody_joints,
self.state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR,
self.harness.state.num_threads(),
));
}
}
}
pub fn set_graphics_shift(&mut self, shift: rapier::math::Vector) {
if !self.state.camera_locked
&& let Some(graphics) = &mut self.graphics
{
graphics.graphics.gfx_shift = shift;
}
}
#[cfg(feature = "dim2")]
pub fn look_at(&mut self, at: glamx::Vec2, zoom: f32) {
if !self.state.camera_locked
&& let Some(graphics) = &mut self.graphics
{
graphics.camera.set_at(at);
graphics.camera.set_zoom(zoom);
}
}
#[cfg(feature = "dim3")]
pub fn look_at(&mut self, eye: Vec3, at: Vec3) {
if !self.state.camera_locked
&& let Some(graphics) = &mut self.graphics
{
graphics.camera.look_at(eye, at);
}
}
pub fn set_initial_body_color(&mut self, body: RigidBodyHandle, color: Color) {
if let Some(graphics) = &mut self.graphics {
graphics.graphics.set_initial_body_color(body, color);
}
}
pub fn set_initial_collider_color(&mut self, collider: ColliderHandle, color: Color) {
if let Some(graphics) = &mut self.graphics {
graphics
.graphics
.set_initial_collider_color(collider, color);
}
}
pub fn set_body_wireframe(&mut self, body: RigidBodyHandle, wireframe_enabled: bool) {
if let Some(graphics) = &mut self.graphics {
graphics
.graphics
.set_body_wireframe(body, wireframe_enabled);
}
}
pub fn add_callback<
F: FnMut(
Option<&mut TestbedGraphics>,
&mut PhysicsState,
&crate::physics::PhysicsEvents,
&crate::harness::RunState,
) + 'static,
>(
&mut self,
callback: F,
) {
self.harness.add_callback(callback);
}
pub fn add_plugin(&mut self, mut plugin: impl crate::plugin::TestbedPlugin + 'static) {
plugin.init_plugin();
self.plugins.0.push(Box::new(plugin));
}
}