bevy_boxddd 0.2.0

Bevy integration for the boxddd Rust bindings to Box3D
Documentation
use crate::control::{DebugDrawPreset, TestbedState};
use crate::lab::LabDiagnostics;
use crate::scenes::{SCENE_REGISTRY, TestbedEntity, TestbedScene};
use crate::{TestbedCamera, switch_scene};
use bevy::prelude::*;
use bevy_egui::egui::{LayerId, Ui, UiBuilder};
use bevy_egui::{EguiContexts, egui};

pub(crate) fn draw_testbed_ui(
    mut contexts: EguiContexts,
    mut state: ResMut<TestbedState>,
    mut commands: Commands,
    entities: Query<Entity, With<TestbedEntity>>,
    mut camera: Query<&mut Transform, With<TestbedCamera>>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    diagnostics: Res<LabDiagnostics>,
) -> Result {
    let mut requested_scene = None;

    let ctx = contexts.ctx_mut()?;
    let mut root_ui = Ui::new(
        ctx.clone(),
        "boxddd_testbed_root".into(),
        UiBuilder::new()
            .layer_id(LayerId::background())
            .max_rect(ctx.viewport_rect()),
    );

    egui::Panel::left("boxddd_testbed_panel")
        .default_size(290.0)
        .min_size(250.0)
        .resizable(true)
        .show(&mut root_ui, |ui| {
            ui.heading(if state.scene_switching_enabled {
                "boxddd Testbed"
            } else {
                SCENE_REGISTRY[state.scene_index].name
            });
            ui.separator();

            ui.horizontal(|ui| {
                if ui
                    .button(if state.paused { "Resume" } else { "Pause" })
                    .clicked()
                {
                    state.paused = !state.paused;
                    if !state.paused {
                        state.cancel_single_step();
                    }
                }
                if ui.button("Reset").clicked() {
                    requested_scene = Some(state.scene_index);
                }
                if ui
                    .add_enabled(state.paused, egui::Button::new("Step"))
                    .clicked()
                {
                    state.request_single_step();
                }
            });

            ui.separator();
            if state.scene_switching_enabled {
                ui.label(egui::RichText::new("Scenes").strong());
                egui::ScrollArea::vertical()
                    .max_height(260.0)
                    .show(ui, |ui| {
                        let mut current_category = None;
                        for (index, metadata) in SCENE_REGISTRY.iter().enumerate() {
                            if current_category != Some(metadata.category) {
                                current_category = Some(metadata.category);
                                ui.add_space(4.0);
                                ui.label(
                                    egui::RichText::new(metadata.category)
                                        .small()
                                        .color(egui::Color32::GRAY),
                                );
                            }

                            if ui
                                .selectable_label(state.scene_index == index, metadata.name)
                                .clicked()
                            {
                                requested_scene = Some(index);
                            }

                            if state.scene_index == index {
                                ui.label(egui::RichText::new(metadata.description).small());
                                ui.label(
                                    egui::RichText::new(metadata.source_label())
                                        .small()
                                        .color(egui::Color32::LIGHT_GRAY),
                                );
                                if let Some(lesson) = metadata.showcase_lesson {
                                    ui.label(egui::RichText::new(lesson).small());
                                }
                            }
                        }
                    });
            } else {
                let metadata = &SCENE_REGISTRY[state.scene_index];
                ui.label(
                    egui::RichText::new(metadata.category)
                        .small()
                        .color(egui::Color32::GRAY),
                );
                ui.label(metadata.description);
                ui.label(
                    egui::RichText::new(metadata.source_label())
                        .small()
                        .color(egui::Color32::LIGHT_GRAY),
                );
                if let Some(lesson) = metadata.showcase_lesson {
                    ui.label(egui::RichText::new(lesson).small());
                }
            }

            ui.separator();
            ui.label(egui::RichText::new("World").strong());
            ui.checkbox(&mut state.gravity_enabled, "Gravity");
            ui.checkbox(&mut state.sleeping_enabled, "Sleeping");
            ui.checkbox(&mut state.warm_starting_enabled, "Warm starting");
            ui.checkbox(&mut state.continuous_enabled, "Continuous collision");
            ui.add(
                egui::Slider::new(
                    &mut state.sub_step_count,
                    crate::control::MIN_SUB_STEPS..=crate::control::MAX_SUB_STEPS,
                )
                .text("Substeps"),
            );
            ui.add(
                egui::Slider::new(
                    &mut state.hertz,
                    crate::control::MIN_HERTZ..=crate::control::MAX_HERTZ,
                )
                .text("Hz"),
            );

            ui.separator();
            ui.label(egui::RichText::new("Debug").strong());
            egui::ComboBox::from_label("Draw")
                .selected_text(state.debug_preset.label())
                .show_ui(ui, |ui| {
                    for preset in DebugDrawPreset::ALL {
                        ui.selectable_value(&mut state.debug_preset, preset, preset.label());
                    }
                });

            draw_scene_lab_controls(ui, state.as_mut(), &diagnostics);
        });

    if let Some(scene_index) = requested_scene {
        switch_scene(
            scene_index,
            &mut state,
            &mut commands,
            &entities,
            &mut camera,
            &mut meshes,
            &mut materials,
        );
    }

    Ok(())
}

fn draw_scene_lab_controls(ui: &mut Ui, state: &mut TestbedState, diagnostics: &LabDiagnostics) {
    match crate::lab::current_scene(state) {
        TestbedScene::QueryLab => {
            ui.separator();
            ui.label(egui::RichText::new("Query Lab").strong());
            ui.add(
                egui::Slider::new(
                    &mut state.query_lab_ray_length,
                    crate::control::MIN_QUERY_RAY_LENGTH..=crate::control::MAX_QUERY_RAY_LENGTH,
                )
                .text("Ray length"),
            );
            ui.add(
                egui::Slider::new(
                    &mut state.query_lab_aabb_half_extent,
                    crate::control::MIN_QUERY_AABB_HALF_EXTENT
                        ..=crate::control::MAX_QUERY_AABB_HALF_EXTENT,
                )
                .text("AABB half extent"),
            );
            ui.add(
                egui::Slider::new(
                    &mut state.query_lab_shape_cast_length,
                    crate::control::MIN_QUERY_SHAPE_CAST_LENGTH
                        ..=crate::control::MAX_QUERY_SHAPE_CAST_LENGTH,
                )
                .text("Shape cast length"),
            );
            ui.add(
                egui::Slider::new(
                    &mut state.query_lab_shape_cast_radius,
                    crate::control::MIN_QUERY_SHAPE_CAST_RADIUS
                        ..=crate::control::MAX_QUERY_SHAPE_CAST_RADIUS,
                )
                .text("Shape radius"),
            );
            ui.add(
                egui::Slider::new(
                    &mut state.query_lab_mover_cast_length,
                    crate::control::MIN_QUERY_MOVER_CAST_LENGTH
                        ..=crate::control::MAX_QUERY_MOVER_CAST_LENGTH,
                )
                .text("Mover length"),
            );
            if diagnostics.query_ray_supported {
                ui.label(format!("Ray hits: {}", diagnostics.query_ray_hit_count));
            } else {
                ui.label("Ray hits: unavailable in browser provider mode");
            }
            if diagnostics.query_overlap_supported {
                ui.label(format!(
                    "Overlap hits: {}",
                    diagnostics.query_overlap_hit_count
                ));
            } else {
                ui.label("Overlap hits: unavailable in browser provider mode");
            }
            ui.label(match diagnostics.query_closest_fraction {
                Some(fraction) => format!("Closest fraction: {fraction:.3}"),
                None => "Closest fraction: none".to_string(),
            });
            if diagnostics.query_shape_cast_supported {
                ui.label(format!(
                    "Shape hits: {}",
                    diagnostics.query_shape_cast_hit_count
                ));
                ui.label(match diagnostics.query_shape_cast_closest_fraction {
                    Some(fraction) => format!("Shape fraction: {fraction:.3}"),
                    None => "Shape fraction: none".to_string(),
                });
            } else {
                ui.label("Shape cast: unavailable in browser provider mode");
            }
            if diagnostics.query_mover_supported {
                ui.label(match diagnostics.query_mover_fraction {
                    Some(fraction) => format!("Mover fraction: {fraction:.3}"),
                    None => "Mover fraction: none".to_string(),
                });
            } else {
                ui.label("Mover fraction: unavailable in browser provider mode");
            }
            if diagnostics.query_mover_planes_supported {
                ui.label(format!(
                    "Mover planes: {}",
                    diagnostics.query_mover_plane_count
                ));
            } else {
                ui.label("Mover planes: unavailable in browser provider mode");
            }
        }
        TestbedScene::DebugDrawInspector => {
            ui.separator();
            ui.label(egui::RichText::new("Debug Draw Inspector").strong());
            ui.horizontal_wrapped(|ui| {
                if ui.button("Shapes").clicked() {
                    state.debug_preset = DebugDrawPreset::Shapes;
                }
                if ui.button("Joints").clicked() {
                    state.debug_preset = DebugDrawPreset::ShapesAndJoints;
                }
                if ui.button("Contacts").clicked() {
                    state.debug_preset = DebugDrawPreset::Contacts;
                }
                if ui.button("Bounds").clicked() {
                    state.debug_preset = DebugDrawPreset::Bounds;
                }
            });
            ui.label(format!("Commands: {}", diagnostics.debug_command_count));
            ui.label(format!("Events: {}", diagnostics.debug_event_count));
            ui.label(format!(
                "Diagnostics: {}",
                diagnostics.debug_diagnostic_count
            ));
        }
        TestbedScene::MaterialLab => {
            ui.separator();
            ui.label(egui::RichText::new("Material Lab").strong());
            ui.add(
                egui::Slider::new(
                    &mut state.material_lab_friction,
                    crate::control::MIN_MATERIAL_FRICTION..=crate::control::MAX_MATERIAL_FRICTION,
                )
                .text("Friction"),
            );
            ui.add(
                egui::Slider::new(
                    &mut state.material_lab_restitution,
                    crate::control::MIN_MATERIAL_RESTITUTION
                        ..=crate::control::MAX_MATERIAL_RESTITUTION,
                )
                .text("Restitution"),
            );
            ui.label(format!(
                "Native shapes: {}",
                diagnostics.material_shape_count
            ));
        }
        TestbedScene::StatsDashboard => {
            ui.separator();
            ui.label(egui::RichText::new("Stats Dashboard").strong());
            ui.label(format!("Fixed step: {:.1} Hz", state.hertz));
            ui.label(format!("Substeps: {}", state.sub_step_count));
            if diagnostics.stats_available {
                ui.label(format!(
                    "Bodies: {} awake {}",
                    diagnostics.stats_body_count, diagnostics.stats_awake_body_count
                ));
                ui.label(format!(
                    "Shapes: {} contacts {} islands {}",
                    diagnostics.stats_shape_count,
                    diagnostics.stats_contact_count,
                    diagnostics.stats_island_count
                ));
                ui.label(format!(
                    "Tree height: {} tasks {}",
                    diagnostics.stats_tree_height, diagnostics.stats_task_count
                ));
                ui.label(format!(
                    "Profile: step {:.3} collide {:.3} solve {:.3}",
                    diagnostics.stats_profile_step,
                    diagnostics.stats_profile_collide,
                    diagnostics.stats_profile_solve
                ));
                ui.label(format!("Pairs: {:.3}", diagnostics.stats_profile_pairs));
            } else {
                ui.label("World stats: unavailable");
            }
        }
        _ => {}
    }
}