symbios-robot
An engine-agnostic robot interpretation layer for Symbios L-Systems.
This crate translates L-System grammars into a RobotBlueprint — a complete description of a robot's topology (rigid bodies, joints, sensors) that can be ingested by game engines, physics simulators, or manufacturing pipelines. It decouples the Genotype (L-System string) from the Phenotype (physics simulation).
Concepts
Genotype → Phenotype
An L-System produces a sequence of symbols and parameters. RobotInterpreter walks that sequence, maintaining a turtle (a cursor with position, orientation, and state), and progressively builds a RobotBlueprint.
L-System String → RobotInterpreter → RobotBlueprint
"B J B(2)" (turtle) 2 modules
1 hinge joint
The Turtle
The turtle state tracks:
- Position / Rotation — where the cursor is in world space
- Current module — the last rigid body spawned (used as the joint parent)
- Active joint config — type, axis, and limits for the next joint
- Width — the default radius/width for shapes
When a geometry symbol is interpreted (e.g. B, C, O, K), a new RobotModule is spawned at the turtle's current position, and the turtle advances to the distal end of the new segment. If a previous module exists, a JointDefinition connecting them is created automatically.
The Blueprint
RobotBlueprint is a plain data structure — no engine dependencies. It contains:
modules: a map ofModuleId → RobotModule(shape, mass, density, material, transform, sensors)joints: a list ofJointDefinition(parent, child, anchors, type, per-axis limits)end_effectors: a list of namedEndEffectorframes (TCP / gripper / probe poses)root_module: the ID of the first module spawned (base of the kinematic chain)
RobotBlueprint is marked #[non_exhaustive] so future additions (new sensor
graphs, cabling, etc.) can be made without breaking downstream consumers.
Construct one via RobotBlueprint::new() / Default and the
add_module / add_joint / add_end_effector helpers — never with a struct
literal.
Joint Types
The drive axis (and screw pitch) live inside the JointType variant, so it
is impossible to construct a joint that has both a "Fixed" type and a stray
axis floating around:
| Variant | DoF | Description |
|---|---|---|
Fixed |
0 | Welded — child is rigidly bonded to parent |
Hinge { axis } |
1 | Rotation about axis (knee, elbow, wheel hub) |
Ball |
3 | Free rotation; constrain via per-axis limits |
Prismatic { axis } |
1 | Translation along axis (linear actuator, telescoping arm) |
Screw { axis, pitch } |
1 | Rotation about axis coupled to translation by pitch m/rev (lead screw) |
Limits are stored as Vec<AxisLimit> — empty means unconstrained, single-axis
joints carry one entry, and Ball joints can carry up to three swing-twist
constraints.
End-Effectors
Tool-center-point frames are declared with the E symbol (parameter: ee_id).
Each EndEffector { id, module_id, local_position, local_rotation } pins a
named frame to the module the turtle is currently standing on. Look up by id:
let gripper = blueprint.end_effector.expect;
Multiple EEs are supported (e.g. 0 = left gripper, 1 = right gripper).
Usage
use ;
use ;
// 1. Set up a symbol table and intern the symbols your grammar uses.
let mut interner = new;
interner.intern.unwrap; // Box segment
interner.intern.unwrap; // Set joint to Hinge
interner.intern.unwrap; // Yaw rotation
// 2. Create an interpreter and register standard symbol mappings.
let config = default;
let mut interpreter = new;
interpreter.populate_standard_symbols;
// 3. Build an L-System state (or derive one from a grammar).
let b_id = interner.resolve_id.unwrap;
let j_id = interner.resolve_id.unwrap;
let mut state = new;
state.push.unwrap; // base segment: length=1, width=0.1, depth=0.1
state.push.unwrap; // switch next joint to Hinge
state.push.unwrap; // forearm segment
// 4. Interpret into a blueprint.
let blueprint = interpreter.build_blueprint;
assert_eq!;
assert_eq!;
Standard Symbol Mappings
The interpreter is built around a sparse Vec<RobotOp> indexed by symbios
symbol ID. Three ways to populate it:
RobotInterpreter::populate_standard_symbols(&interner)— registers the conventional symbol→op mappings shown below for every symbol that has been interned. Symbols that were never interned are silently skipped.RobotInterpreter::set_op(sym_id, op)— assigns a single op; the map is grown as needed and gaps are filled withRobotOp::Ignore.RobotInterpreter::with_map(map)— replaces the entire op map in one shot (builder style); useful when you've precomputed the full mapping table.
Set-joint-type entries use JointTypeKind (a payload-free tag enum) so the
mapping table doesn't have to invent a placeholder axis or pitch — those are
filled in from the live turtle's ActiveJointConfig when the joint is built.
| Symbol | Operation | Parameters |
|---|---|---|
f |
Move forward (no geometry) | (length) |
+ |
Yaw +1× default angle | (angle_deg) override |
- |
Yaw −1× default angle | (angle_deg) override |
& |
Pitch +1× default angle | (angle_deg) override |
^ |
Pitch −1× default angle | (angle_deg) override |
\ |
Roll +1× default angle | (angle_deg) override |
/ |
Roll −1× default angle | (angle_deg) override |
| |
Turn around 180° | — |
B |
Spawn Box | (length, width, depth) |
C |
Spawn Cylinder | (length, radius) |
O |
Spawn Sphere | (radius) |
K |
Spawn Capsule | (length, radius) |
! |
Set default width/radius | (width) |
' |
Set material ID | (material_id) |
J |
Set next joint → Hinge | — |
Jf |
Set next joint → Fixed | — |
Jb |
Set next joint → Ball | — |
Jp |
Set next joint → Prismatic | — |
Js |
Set next joint → Screw | — |
Ja |
Set staging joint axis | (ax, ay, az) |
Jh |
Set screw pitch (helix) | (pitch_m_per_rev) |
Jl |
Append axis limit (current axis) | (min, max, effort, velocity) |
Jla |
Append axis limit (explicit axis) | (ax, ay, az, min, max, effort, velocity) |
Jlc |
Clear accumulated joint limits | — |
S |
Mount Camera sensor | — |
Si |
Mount IMU sensor | — |
St |
Mount Touch sensor | — |
Sl |
Mount Lidar sensor | — |
Su |
Mount Ultrasonic sensor | — |
E |
Mark end-effector / TCP | (ee_id) |
[ |
Push turtle state | — |
] |
Pop turtle state | — |
Configuration
RobotConfig controls interpreter defaults:
| Field | Default | Description |
|---|---|---|
default_length |
1.0 m |
Segment length (along growth axis) when no parameter given |
default_width |
0.2 m |
Initial turtle width / lateral extent when no parameter given |
default_density |
100.0 kg/m³ |
Density used by bevy_heavy to derive each module's mass |
default_angle |
45° |
Rotation step for +, -, &, ^, \, / |
max_stack_depth |
1024 |
Maximum push/pop nesting depth (excess pushes are silently dropped) |
Bounding Box
RobotBlueprint::aabb(rotation) computes the axis-aligned bounding box of the entire robot in its rest pose, optionally rotated by rotation.
use Quat;
let aabb = blueprint.aabb;
println!;
Dependencies
symbios— L-System engine providingSymbiosStateandSymbolTableglam— Math primitives (Vec3,Quat)bevy_math— Geometric primitives and bounding volume computationbevy_heavy— Mass property computation from shape geometryserde— Serialization of the blueprint
License
MIT