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:
658
src_testbed/testbed/app.rs
Normal file
658
src_testbed/testbed/app.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
94
src_testbed/testbed/graphics_context.rs
Normal file
94
src_testbed/testbed/graphics_context.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
64
src_testbed/testbed/hover.rs
Normal file
64
src_testbed/testbed/hover.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src_testbed/testbed/keys.rs
Normal file
24
src_testbed/testbed/keys.rs
Normal 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
|
||||
}
|
||||
}
|
||||
35
src_testbed/testbed/mod.rs
Normal file
35
src_testbed/testbed/mod.rs
Normal 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>>);
|
||||
159
src_testbed/testbed/state.rs
Normal file
159
src_testbed/testbed/state.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
243
src_testbed/testbed/testbed.rs
Normal file
243
src_testbed/testbed/testbed.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user