Skip to main content

symbios_robot/
blueprint.rs

1use bevy_heavy::ComputeMassProperties3d;
2use bevy_math::Isometry3d;
3use bevy_math::bounding::{Aabb3d, Bounded3d, BoundingSphere, BoundingVolume};
4use bevy_math::primitives::{Capsule3d, Cuboid, Cylinder, Sphere};
5use glam::{Quat, Vec3};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// A unique identifier for a robot module (rigid body).
10/// Maps to L-System derivation steps or Turtle spawn indices.
11pub type ModuleId = u16;
12
13/// A generic material identifier referencing an external palette.
14///
15/// 16-bit to support large multi-material assemblies (sensor housings, fasteners,
16/// PCB substrates, etc.) where the legacy 8-bit cap (256 entries) was restrictive.
17pub type MaterialId = u16;
18
19/// A unique identifier for a named end-effector / TCP frame.
20pub type EndEffectorId = u16;
21
22/// The complete, engine-agnostic definition of a robot's topology.
23///
24/// This structure represents the "Phenotype" generated from an L-System.
25/// It contains a graph of rigid bodies (modules) connected by joints.
26///
27/// Marked `#[non_exhaustive]` so that new fields can be added without breaking
28/// downstream code that matches or constructs this type.
29#[non_exhaustive]
30#[derive(Clone, Debug, Default, Serialize, Deserialize)]
31pub struct RobotBlueprint {
32    /// The ID of the root module (base of the robot).
33    pub root_module: Option<ModuleId>,
34
35    /// All rigid bodies in the robot, indexed by their unique ID.
36    pub modules: HashMap<ModuleId, RobotModule>,
37
38    /// All physical connections between modules.
39    pub joints: Vec<JointDefinition>,
40
41    /// Named end-effector / TCP frames declared on the robot.
42    ///
43    /// Multiple EEs are supported (e.g. left/right gripper). Each entry pins a
44    /// pose offset to a specific module so IK and grasp planners can resolve a
45    /// tool-center-point pose from joint angles.
46    pub end_effectors: Vec<EndEffector>,
47}
48
49impl RobotBlueprint {
50    /// Creates an empty blueprint with no modules or joints.
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Inserts a module into the blueprint.
56    ///
57    /// The first module inserted is automatically set as [`root_module`](Self::root_module).
58    /// If `id` already exists it is silently overwritten.
59    pub fn add_module(&mut self, id: ModuleId, module: RobotModule) {
60        if self.modules.is_empty() {
61            self.root_module = Some(id);
62        }
63        self.modules.insert(id, module);
64    }
65
66    /// Appends a joint to the blueprint.
67    pub fn add_joint(&mut self, joint: JointDefinition) {
68        self.joints.push(joint);
69    }
70
71    /// Registers an end-effector frame on the blueprint.
72    pub fn add_end_effector(&mut self, ee: EndEffector) {
73        self.end_effectors.push(ee);
74    }
75
76    /// Looks up an end-effector by its numeric ID.
77    pub fn end_effector(&self, id: EndEffectorId) -> Option<&EndEffector> {
78        self.end_effectors.iter().find(|e| e.id == id)
79    }
80
81    /// Compute the axis-aligned bounding box of the entire robot
82    /// after applying `rotation` to the blueprint's rest pose.
83    pub fn aabb(&self, rotation: Quat) -> Aabb3d {
84        let mut combined: Option<Aabb3d> = None;
85        for module in self.modules.values() {
86            let (pos, rot) = module.transform;
87            let isometry = Isometry3d::new(rotation * pos, rotation * rot);
88            let aabb = module.shape.to_bevy_primitive().aabb_3d(isometry);
89            combined = Some(match combined {
90                Some(c) => c.merge(&aabb),
91                None => aabb,
92            });
93        }
94        combined.unwrap_or(Aabb3d::new(Vec3::ZERO, Vec3::ZERO))
95    }
96}
97
98/// A single rigid body segment of the robot.
99#[derive(Clone, Debug, Serialize, Deserialize)]
100pub struct RobotModule {
101    /// The physical shape of this segment.
102    pub shape: ShapePrimitive,
103
104    /// Mass in kg, computed from shape volume and density via `bevy_heavy`.
105    pub mass: f32,
106
107    /// Density in kg/m³ used to derive mass properties.
108    pub density: f32,
109
110    /// Material ID for visual rendering (links to external palette).
111    pub material_id: MaterialId,
112
113    /// Sensors attached directly to this module.
114    pub sensors: Vec<SensorMount>,
115
116    /// Initial World Transform (Position, Rotation) for the Rest Pose.
117    /// Essential for stable physics initialization.
118    pub transform: (Vec3, Quat),
119}
120
121/// Supported geometric primitives for robot segments.
122#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
123pub enum ShapePrimitive {
124    /// A box defined by half-extents (x, y, z).
125    Box(Vec3),
126    /// A cylinder defined by radius and height (aligned along Y axis).
127    Cylinder { radius: f32, height: f32 },
128    /// A sphere defined by radius.
129    Sphere(f32),
130    /// A capsule defined by radius and height (aligned along Y axis).
131    Capsule { radius: f32, height: f32 },
132}
133
134/// A type-erased wrapper around `bevy_math` primitives.
135///
136/// Enables calling [`bevy_heavy::ComputeMassProperties3d`] and
137/// [`bevy_math::bounding::Bounded3d`] on any [`ShapePrimitive`] variant through a
138/// single enum dispatch. Obtain one via [`ShapePrimitive::to_bevy_primitive`].
139#[derive(Clone, Copy, Debug)]
140pub enum BevyPrimitive {
141    Cuboid(Cuboid),
142    Cylinder(Cylinder),
143    Sphere(Sphere),
144    Capsule(Capsule3d),
145}
146
147impl ComputeMassProperties3d for BevyPrimitive {
148    fn mass(&self, density: f32) -> f32 {
149        match self {
150            Self::Cuboid(s) => s.mass(density),
151            Self::Cylinder(s) => s.mass(density),
152            Self::Sphere(s) => s.mass(density),
153            Self::Capsule(s) => s.mass(density),
154        }
155    }
156
157    fn unit_principal_angular_inertia(&self) -> Vec3 {
158        match self {
159            Self::Cuboid(s) => s.unit_principal_angular_inertia(),
160            Self::Cylinder(s) => s.unit_principal_angular_inertia(),
161            Self::Sphere(s) => s.unit_principal_angular_inertia(),
162            Self::Capsule(s) => s.unit_principal_angular_inertia(),
163        }
164    }
165
166    fn center_of_mass(&self) -> Vec3 {
167        match self {
168            Self::Cuboid(s) => s.center_of_mass(),
169            Self::Cylinder(s) => s.center_of_mass(),
170            Self::Sphere(s) => s.center_of_mass(),
171            Self::Capsule(s) => s.center_of_mass(),
172        }
173    }
174}
175
176impl Bounded3d for BevyPrimitive {
177    fn aabb_3d(&self, isometry: impl Into<Isometry3d>) -> Aabb3d {
178        match self {
179            Self::Cuboid(s) => s.aabb_3d(isometry),
180            Self::Cylinder(s) => s.aabb_3d(isometry),
181            Self::Sphere(s) => s.aabb_3d(isometry),
182            Self::Capsule(s) => s.aabb_3d(isometry),
183        }
184    }
185
186    fn bounding_sphere(&self, isometry: impl Into<Isometry3d>) -> BoundingSphere {
187        match self {
188            Self::Cuboid(s) => s.bounding_sphere(isometry),
189            Self::Cylinder(s) => s.bounding_sphere(isometry),
190            Self::Sphere(s) => s.bounding_sphere(isometry),
191            Self::Capsule(s) => s.bounding_sphere(isometry),
192        }
193    }
194}
195
196impl ShapePrimitive {
197    /// Convert to the corresponding `bevy_math` primitive for mass-property computation.
198    pub fn to_bevy_primitive(self) -> BevyPrimitive {
199        match self {
200            Self::Box(half_extents) => BevyPrimitive::Cuboid(Cuboid {
201                half_size: half_extents,
202            }),
203            Self::Cylinder { radius, height } => {
204                BevyPrimitive::Cylinder(Cylinder::new(radius, height))
205            }
206            Self::Sphere(r) => BevyPrimitive::Sphere(Sphere::new(r)),
207            Self::Capsule { radius, height } => {
208                BevyPrimitive::Capsule(Capsule3d::new(radius, height))
209            }
210        }
211    }
212}
213
214/// A kinematic connection between two modules.
215///
216/// The drive axis (for single-axis joints) is carried on the [`JointType`] payload
217/// so it cannot be set on a joint that has no axis. Per-axis motion limits are
218/// stored in [`JointDefinition::limits`] as a vector — empty means "unlimited",
219/// a single entry covers a single-axis joint, and up to three entries can pin
220/// the swing-twist constraints on a [`JointType::Ball`].
221#[derive(Clone, Debug, Serialize, Deserialize)]
222pub struct JointDefinition {
223    /// The parent module (the one closer to the root).
224    pub parent_id: ModuleId,
225
226    /// The child module (the one attached to the parent).
227    pub child_id: ModuleId,
228
229    /// The anchor point on the parent module, in parent's local space.
230    pub anchor_parent: Vec3,
231
232    /// The anchor point on the child module, in child's local space.
233    pub anchor_child: Vec3,
234
235    /// The type of mechanical connection. Carries the drive axis (if any).
236    pub joint_type: JointType,
237
238    /// Per-axis physical limits. Empty = unlimited.
239    pub limits: Vec<AxisLimit>,
240}
241
242/// Types of mechanical joints.
243///
244/// Variants carry the data that is *intrinsic* to the joint type:
245/// - [`Fixed`](Self::Fixed) and [`Ball`](Self::Ball) have no drive axis.
246/// - [`Hinge`](Self::Hinge), [`Prismatic`](Self::Prismatic), and [`Screw`](Self::Screw) carry a single axis in
247///   the parent module's local space.
248/// - [`Screw`](Self::Screw) additionally carries a `pitch` (meters of linear travel per
249///   full revolution; positive = right-handed thread).
250#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
251pub enum JointType {
252    /// Fixed connection (welded). No degrees of freedom.
253    Fixed,
254    /// Rotates around a single axis (e.g., knee, elbow).
255    Hinge { axis: Vec3 },
256    /// Ball and socket (3 rotational degrees of freedom).
257    Ball,
258    /// Slides along a single axis (linear actuator).
259    Prismatic { axis: Vec3 },
260    /// Helical joint: rotation about `axis` is coupled to translation along it
261    /// by `pitch` meters per full revolution. Models lead screws and screw-driven
262    /// linear stages.
263    Screw { axis: Vec3, pitch: f32 },
264}
265
266impl JointType {
267    /// Returns the drive axis for single-axis joints, or `None` for [`Fixed`](Self::Fixed) / [`Ball`](Self::Ball).
268    pub fn axis(&self) -> Option<Vec3> {
269        match self {
270            Self::Fixed | Self::Ball => None,
271            Self::Hinge { axis } | Self::Prismatic { axis } | Self::Screw { axis, .. } => {
272                Some(*axis)
273            }
274        }
275    }
276
277    /// Returns the discriminant kind (no axis / pitch payload).
278    pub fn kind(&self) -> JointTypeKind {
279        match self {
280            Self::Fixed => JointTypeKind::Fixed,
281            Self::Hinge { .. } => JointTypeKind::Hinge,
282            Self::Ball => JointTypeKind::Ball,
283            Self::Prismatic { .. } => JointTypeKind::Prismatic,
284            Self::Screw { .. } => JointTypeKind::Screw,
285        }
286    }
287}
288
289/// Tag enum naming a joint variant *without* axis/pitch payload.
290///
291/// Used by the turtle layer to decouple "which kind of joint do I want next"
292/// (a static decision baked into a symbol mapping) from the axis/pitch values
293/// that get filled in from the live turtle state when the joint is built.
294#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
295pub enum JointTypeKind {
296    Fixed,
297    Hinge,
298    Ball,
299    Prismatic,
300    Screw,
301}
302
303/// Motion limits along (or about) a single axis.
304///
305/// For [`JointType::Hinge`] / [`JointType::Screw`] limits are in radians and the
306/// `effort` is a torque (Nm). For [`JointType::Prismatic`] limits are in meters
307/// and `effort` is a force (N). Multiple `AxisLimit`s can be attached to a
308/// [`JointType::Ball`] to express swing-twist cones.
309#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
310pub struct AxisLimit {
311    /// Axis (in parent local space) that this limit constrains.
312    pub axis: Vec3,
313    /// Minimum angle (radians) or distance (meters).
314    pub min: f32,
315    /// Maximum angle (radians) or distance (meters).
316    pub max: f32,
317    /// Maximum torque (Nm) or force (N) the motor can apply about/along this axis.
318    pub effort: f32,
319    /// Maximum velocity (rad/s or m/s) about/along this axis.
320    pub velocity: f32,
321}
322
323/// A named end-effector / tool-center-point frame attached to a module.
324///
325/// IK solvers, grasp planners, and tooling pipelines consume this frame to
326/// resolve "where the gripper / probe / nozzle actually is" relative to the
327/// kinematic chain.
328#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
329pub struct EndEffector {
330    /// Stable identifier (e.g. 0 = primary, 1 = secondary). Used by external
331    /// tooling to address the frame.
332    pub id: EndEffectorId,
333    /// The module the EE is rigidly attached to.
334    pub module_id: ModuleId,
335    /// EE pose offset relative to the module's center (position + orientation).
336    pub local_position: Vec3,
337    pub local_rotation: Quat,
338}
339
340/// A sensor attachment point.
341#[derive(Clone, Debug, Serialize, Deserialize)]
342pub struct SensorMount {
343    /// Type of sensor (Camera, Lidar, Touch, IMU).
344    pub sensor_type: SensorType,
345
346    /// Position relative to the module's center.
347    pub local_position: Vec3,
348
349    /// Orientation relative to the module.
350    pub local_rotation: Quat,
351}
352
353/// The kind of sensor mounted on a module.
354#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
355pub enum SensorType {
356    /// RGB or depth camera.
357    Camera,
358    /// Laser range scanner.
359    Lidar,
360    /// Contact / pressure sensor.
361    Touch,
362    /// Inertial measurement unit (accelerometer + gyroscope).
363    IMU,
364    /// Ultrasonic distance sensor.
365    Ultrasonic,
366}