use crate::materials::MaterialPalette;
use avian3d::prelude::*;
use bevy::prelude::*;
use symbios_robot::{AxisLimit, JointType, RobotBlueprint, SensorType, ShapePrimitive};
fn limit_for_axis(limits: &[AxisLimit], drive_axis: Vec3) -> Option<&AxisLimit> {
let drive = drive_axis.normalize_or_zero();
limits
.iter()
.find(|l| l.axis.normalize_or_zero().dot(drive).abs() > 0.999)
.or_else(|| limits.first())
}
#[derive(Component)]
pub struct ImuSensor;
#[derive(Component)]
pub struct TouchSensor;
pub struct SpawnedRobot {
pub modules: Vec<Entity>,
pub joints: Vec<(Entity, JointType)>,
pub sensors: Vec<(Entity, SensorType)>,
}
pub fn spawn_robot(
commands: &mut Commands,
blueprint: &RobotBlueprint,
palette: &MaterialPalette,
meshes: &mut Assets<Mesh>,
spawn_location: Transform,
) -> SpawnedRobot {
let mut entity_map: bevy::platform::collections::HashMap<_, (Entity, Quat)> =
bevy::platform::collections::HashMap::new();
let mut module_entities = Vec::new();
let mut joint_entities = Vec::new();
let mut sensor_entries = Vec::new();
let mut sorted_modules: Vec<_> = blueprint.modules.iter().collect();
sorted_modules.sort_by_key(|(id, _)| *id);
for (mod_id, module) in &sorted_modules {
let (pos, rot) = module.transform;
let initial_transform =
spawn_location * Transform::from_translation(pos).with_rotation(rot);
let mesh_handle = match module.shape {
ShapePrimitive::Box(e) => meshes.add(Cuboid::from_size(e * 2.0)),
ShapePrimitive::Cylinder { radius, height } => {
meshes.add(Cylinder::new(radius, height))
}
ShapePrimitive::Sphere(r) => meshes.add(Sphere::new(r)),
ShapePrimitive::Capsule { radius, height } => {
meshes.add(Capsule3d::new(radius, height))
}
};
let collider = match module.shape {
ShapePrimitive::Box(e) => Collider::cuboid(e.x * 2.0, e.y * 2.0, e.z * 2.0),
ShapePrimitive::Cylinder { radius, height } => Collider::cylinder(radius, height),
ShapePrimitive::Sphere(r) => Collider::sphere(r),
ShapePrimitive::Capsule { radius, height } => Collider::capsule(radius, height),
};
let material = palette
.materials
.get(&module.material_id)
.unwrap_or(&palette.primary_material)
.clone();
let mass_props =
MassPropertiesBundle::from_shape(&module.shape.to_bevy_primitive(), module.density);
let entity = commands
.spawn((
RigidBody::Dynamic,
collider,
mass_props,
Mesh3d(mesh_handle),
MeshMaterial3d(material),
initial_transform,
))
.id();
entity_map.insert(*mod_id, (entity, rot));
module_entities.push(entity);
for sensor in &module.sensors {
match sensor.sensor_type {
SensorType::IMU => {
commands.entity(entity).insert(ImuSensor);
sensor_entries.push((entity, SensorType::IMU));
}
SensorType::Touch => {
commands.entity(entity).insert(TouchSensor);
sensor_entries.push((entity, SensorType::Touch));
}
_ => {}
}
}
}
for joint_def in &blueprint.joints {
let &(parent_entity, parent_rot) = entity_map
.get(&joint_def.parent_id)
.expect("Parent module missing");
let &(child_entity, child_rot) = entity_map
.get(&joint_def.child_id)
.expect("Child module missing");
let child_rest_basis = child_rot.inverse() * parent_rot;
let joint_entity = match joint_def.joint_type {
JointType::Fixed => commands
.spawn(
FixedJoint::new(parent_entity, child_entity)
.with_local_anchor1(joint_def.anchor_parent)
.with_local_anchor2(joint_def.anchor_child)
.with_local_basis2(child_rest_basis),
)
.id(),
JointType::Hinge { axis } => {
let mut joint = RevoluteJoint::new(parent_entity, child_entity)
.with_local_anchor1(joint_def.anchor_parent)
.with_local_anchor2(joint_def.anchor_child)
.with_local_basis2(child_rest_basis)
.with_hinge_axis(axis);
if let Some(limit) = limit_for_axis(&joint_def.limits, axis) {
joint = joint.with_angle_limits(limit.min, limit.max);
let motor = AngularMotor::default()
.with_max_torque(limit.effort)
.with_target_velocity(limit.velocity);
joint.motor = motor;
}
commands.spawn(joint).id()
}
JointType::Ball => commands
.spawn(
SphericalJoint::new(parent_entity, child_entity)
.with_local_anchor1(joint_def.anchor_parent)
.with_local_anchor2(joint_def.anchor_child)
.with_local_basis2(child_rest_basis),
)
.id(),
JointType::Prismatic { axis } => {
let mut joint = PrismaticJoint::new(parent_entity, child_entity)
.with_local_anchor1(joint_def.anchor_parent)
.with_local_anchor2(joint_def.anchor_child)
.with_local_basis2(child_rest_basis)
.with_slider_axis(axis);
if let Some(limit) = limit_for_axis(&joint_def.limits, axis) {
joint = joint.with_limits(limit.min, limit.max);
let motor = LinearMotor::default()
.with_max_force(limit.effort)
.with_target_velocity(limit.velocity);
joint.motor = motor;
}
commands.spawn(joint).id()
}
JointType::Screw { .. } => {
warn!(
"Screw joints are not supported by avian3d; spawning Fixed joint between {:?} and {:?}",
joint_def.parent_id, joint_def.child_id
);
commands
.spawn(
FixedJoint::new(parent_entity, child_entity)
.with_local_anchor1(joint_def.anchor_parent)
.with_local_anchor2(joint_def.anchor_child)
.with_local_basis2(child_rest_basis),
)
.id()
}
};
joint_entities.push((joint_entity, joint_def.joint_type));
}
SpawnedRobot {
modules: module_entities,
joints: joint_entities,
sensors: sensor_entries,
}
}