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}