use bevy::{
picking::pointer::{PointerInteraction, PointerPress},
prelude::*,
};
use bevy_inspector_egui::{
bevy_egui::EguiContext,
egui::{self, ScrollArea, TextFormat, Ui, text::LayoutJob},
};
use bevy_rapier3d::prelude::*;
use bevy_serialization_core::prelude::*;
use bevy_serialization_physics::prelude::*;
use bevy::prelude::Vec3;
use bevy_ui_extras::{UiExtrasDebug, systems::visualize_components_for};
use bevy_window::PrimaryWindow;
use bitvec::{field::BitField, order::Msb0, view::BitView};
use strum::IntoEnumIterator;
use strum_macros::{Display, EnumIter};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(MeshPickingPlugin)
.add_plugins((
RapierPhysicsPlugin::<NoUserData>::default(),
RapierDebugRenderPlugin::default(),
))
.register_type::<Selectable>()
.register_type::<Selected>()
.add_plugins(UiExtrasDebug::default())
.add_plugins(SerializationPlugin)
.add_plugins(SerializationPhysicsPlugin)
.add_plugins(SerializationBasePlugin)
.register_type::<Selectable>()
.init_resource::<SelectedMotorAxis>()
.init_resource::<PhysicsUtilitySelection>()
.add_systems(
Update,
visualize_components_for::<Selected>(bevy_ui_extras::Display::Window),
)
.add_systems(Update, selection_behaviour)
.add_systems(Startup, setup_graphics)
.add_systems(Startup, create_revolute_joints)
.add_systems(Update, motor_controller_ui)
.add_systems(Update, physics_utilities_ui)
.add_systems(Update, rapier_joint_info_ui)
.run();
}
fn setup_graphics(mut commands: Commands) {
commands.spawn((
Camera3d::default(),
Transform::from_xyz(ORIGIN.x - 5.0, ORIGIN.y, ORIGIN.z)
.looking_at(Vec3::new(13.0, 1.0, 1.0), Vec3::Y),
));
}
const DASHES: usize = 5;
const CUBE_COUNT: usize = 2;
const ORIGIN: Vec3 = Vec3::new(0.0, 0.0, 0.0);
const NUM: usize = 1;
#[derive(Component, Reflect)]
pub struct Selectable;
#[derive(Resource, Default, Clone, Copy, PartialEq)]
pub struct SelectedMotorAxis {
pub axis: MotorAxis,
}
#[derive(Component, Default, Reflect)]
#[reflect(Component)]
pub struct Selected;
#[derive(Default, EnumIter, Clone, Copy, Display, PartialEq)]
pub enum MotorAxis {
X = 0,
Y = 1,
Z = 2,
#[default]
ANGX = 3,
ANGY = 4,
ANGZ = 5,
}
#[derive(Default, EnumIter, Display)]
pub enum PhysicsUtilityType {
#[default]
Joints,
}
#[derive(Resource, Default)]
pub struct PhysicsUtilitySelection {
pub selected: PhysicsUtilityType,
}
fn create_revolute_joints(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let shift = 2.0;
let mut curr_parent = commands
.spawn((
RigidBody::Fixed,
Name::new("joint root"),
Mesh3d(meshes.add(Cuboid::new(0.5, 0.5, 0.5))),
Transform::from_xyz(ORIGIN.x, ORIGIN.y, 0.0),
MeshMaterial3d(materials.add(Color::Srgba(Srgba::BLUE))),
AsyncCollider::default(),
))
.id();
for i in 0..NUM {
let z = ORIGIN.z + i as f32 * shift * 2.0 + shift;
let positions = [
Vec3::new(ORIGIN.x, ORIGIN.y, z),
Vec3::new(ORIGIN.x + shift, ORIGIN.y, z),
Vec3::new(ORIGIN.x + shift, ORIGIN.y, z + shift),
Vec3::new(ORIGIN.x, ORIGIN.y, z + shift),
];
let mut handles = [curr_parent; CUBE_COUNT];
for k in 0..CUBE_COUNT {
handles[k] = commands
.spawn((
RigidBody::Dynamic,
Mesh3d(meshes.add(Cuboid::new(0.5, 0.5, 0.5))),
Transform::from_translation(positions[k]),
MeshMaterial3d(materials.add(Color::Srgba(Srgba::BLUE))),
Collider::cuboid(0.5, 0.5, 0.5),
))
.id();
}
let x = Vec3::X;
let z = Vec3::Z;
let revs = [
RevoluteJointBuilder::new(z)
.local_anchor2(Vec3::new(0.0, 0.0, -shift))
.motor_velocity(100.0, 20.0),
RevoluteJointBuilder::new(x).local_anchor2(Vec3::new(-shift, 0.0, 0.0)),
];
commands
.entity(handles[0])
.insert(ImpulseJoint::new(curr_parent, revs[0]))
.insert(Selectable);
commands
.entity(handles[1])
.insert(ImpulseJoint::new(handles[0], revs[1]))
.insert(Selectable);
curr_parent = *handles.last().unwrap();
}
}
pub fn selection_behaviour(
pointers: Query<(&PointerInteraction, &PointerPress)>,
mut commands: Commands,
selected: Query<&Selected>,
mouse_press: Res<ButtonInput<MouseButton>>,
) {
let Ok((hits, press)) = pointers
.get_single()
.inspect_err(|err| warn!("error: {:#}", err))
else {
return;
};
if press.is_primary_pressed() && mouse_press.just_pressed(MouseButton::Left) {
let Some((e, _)) = hits.first() else {
return;
};
let e = *e;
if selected.contains(e) {
commands.entity(e).remove::<Selected>();
} else {
commands.entity(e).insert(Selected);
}
}
}
pub fn rapier_joint_info_ui(
mut rapier_joint_window: Query<&mut EguiContext, With<PrimaryWindow>>,
mut rapier_joints: Query<&ImpulseJoint, With<Selected>>,
) {
for mut context in rapier_joint_window.iter_mut() {
egui::Window::new("Rapier Joint Info textbox")
.show(context.get_mut(), |ui| {
for joint in rapier_joints.iter_mut() {
ScrollArea::vertical()
.max_height(500.0)
.show(ui, |ui| {
let joint_as_string = format!("{:#?}", joint);
let job =
LayoutJob::single_section(joint_as_string, TextFormat::default());
if ui.button("Copy to clipboard").clicked() {
ui.output_mut(|o| o.copied_text = String::from(job.text.clone()));
}
ui.label(job.clone());
});
}
});
}
}
pub fn motor_controller_ui(
mut selected_joints: Query<(Entity, &mut JointFlag), With<Selected>>,
keyboard: Res<ButtonInput<KeyCode>>,
mut contexts: Query<&mut EguiContext, With<PrimaryWindow>>,
mut motor_axis: ResMut<SelectedMotorAxis>,
) {
let negative_accel_key = KeyCode::Minus;
let positive_accel_key = KeyCode::Equal;
let negative_damping_key = KeyCode::BracketLeft;
let positive_damping_key = KeyCode::BracketRight;
let motor_index = motor_axis.axis as usize;
let window_name = "motor controller";
for mut context in contexts.iter_mut() {
egui::Window::new(window_name)
.show(context.get_mut(), |ui| {
ui.label("Controls");
ui.label("-".repeat(DASHES));
ui.label(format!(
"positive acceleration: [{:#?}] key",
positive_accel_key
));
ui.label(format!(
"negative acceleration: [{:#?}] key",
negative_accel_key
));
ui.label("-".repeat(DASHES));
for (e, joint) in selected_joints.iter() {
ui.label(format!("{:#?}, to {:#?} info", e, joint.parent));
ui.label("motor info:");
ui.horizontal(|ui| {
for axis in MotorAxis::iter() {
ui.selectable_value(&mut motor_axis.axis, axis, axis.to_string());
}
});
ui.label(format!("{:#?}", joint.joint.motors[motor_index]));
ui.label("-".repeat(DASHES));
}
});
}
if keyboard.pressed(negative_accel_key) {
for (_, mut joint) in selected_joints.iter_mut() {
joint.joint.motors[motor_index].target_vel += -1.0;
}
}
if keyboard.pressed(positive_accel_key) {
for (_, mut joint) in selected_joints.iter_mut() {
joint.joint.motors[motor_index].target_vel += 1.0;
}
}
if keyboard.pressed(negative_damping_key) {
for (_, mut joint) in selected_joints.iter_mut() {
joint.joint.motors[motor_index].damping += -1.0;
}
}
if keyboard.pressed(positive_damping_key) {
for (_, mut joint) in selected_joints.iter_mut() {
joint.joint.motors[motor_index].damping += 1.0;
}
}
}
pub fn physics_utilities_ui(
mut primary_window: Query<&mut EguiContext, With<PrimaryWindow>>,
mut utility_selection: ResMut<PhysicsUtilitySelection>,
mut selected_joints: Query<(Entity, &mut JointFlag), With<Selected>>,
) {
for mut context in primary_window.iter_mut() {
egui::Window::new("Physics Utilities")
.show(context.get_mut(), |ui| {
ui.horizontal(|ui| {
for utility in PhysicsUtilityType::iter() {
if ui.button(utility.to_string()).clicked() {
utility_selection.selected = utility;
}
}
});
match utility_selection.selected {
PhysicsUtilityType::Joints => {
for (e, mut joint) in selected_joints.iter_mut() {
ui.label(format!("{:#?}, to {:#?} info", e, joint.parent));
ui.label("-".repeat(DASHES));
ui.label("limit axis bits");
ui.horizontal(|ui: &mut Ui| {
let mut limit_axis_bits = joint.joint.limit_axes.bits().clone();
let limit_axis_bitvec = limit_axis_bits.view_bits_mut::<Msb0>();
for mut bit in limit_axis_bitvec.iter_mut() {
ui.checkbox(&mut bit, "");
}
let new_joint_mask = JointAxesMaskWrapper::from_bits_truncate(
limit_axis_bitvec.load_le(),
);
if joint.joint.limit_axes != new_joint_mask {
joint.joint.limit_axes = new_joint_mask;
}
});
ui.label("locked axis bits");
ui.horizontal(|ui| {
let mut locked_axis_bits = joint.joint.locked_axes.bits().clone();
let limit_axis_bitvec = locked_axis_bits.view_bits_mut::<Msb0>();
for mut bit in limit_axis_bitvec.iter_mut() {
ui.checkbox(&mut bit, "");
}
let new_joint_mask = JointAxesMaskWrapper::from_bits_truncate(
limit_axis_bitvec.load_le(),
);
if joint.joint.locked_axes != new_joint_mask {
joint.joint.locked_axes = new_joint_mask;
}
});
ui.label("-".repeat(DASHES));
}
}
}
});
}
}