use std::collections::HashMap;
#[allow(unused_imports)]
use super::functions::*;
use super::functions::{Mat4, ROS_MAGIC, Vec3};
#[derive(Debug, Clone, Default)]
pub struct JointStateMsg {
pub timestamp: f64,
pub names: Vec<String>,
pub positions: Vec<f64>,
pub velocities: Vec<f64>,
pub efforts: Vec<f64>,
}
impl JointStateMsg {
pub fn new(
timestamp: f64,
names: Vec<String>,
positions: Vec<f64>,
velocities: Vec<f64>,
efforts: Vec<f64>,
) -> Self {
debug_assert_eq!(names.len(), positions.len());
debug_assert_eq!(names.len(), velocities.len());
debug_assert_eq!(names.len(), efforts.len());
Self {
timestamp,
names,
positions,
velocities,
efforts,
}
}
pub fn len(&self) -> usize {
self.names.len()
}
pub fn is_empty(&self) -> bool {
self.names.is_empty()
}
pub fn index_of(&self, name: &str) -> Option<usize> {
self.names.iter().position(|n| n == name)
}
pub fn position_of(&self, name: &str) -> Option<f64> {
self.index_of(name).map(|i| self.positions[i])
}
pub fn unit_kinetic_energy(&self) -> f64 {
self.velocities.iter().map(|v| 0.5 * v * v).sum()
}
pub fn to_csv_line(&self) -> String {
let fields: Vec<String> = self
.names
.iter()
.enumerate()
.map(|(i, n)| {
format!(
"{}:{:.6}:{:.6}:{:.6}",
n, self.positions[i], self.velocities[i], self.efforts[i]
)
})
.collect();
format!("{:.6},{}", self.timestamp, fields.join(","))
}
pub fn to_bytes(&self) -> Vec<u8> {
let n = self.names.len();
let mut buf = Vec::with_capacity(8 + n * 24);
buf.extend_from_slice(&(self.timestamp).to_le_bytes());
for i in 0..n {
buf.extend_from_slice(&self.positions[i].to_le_bytes());
buf.extend_from_slice(&self.velocities[i].to_le_bytes());
buf.extend_from_slice(&self.efforts[i].to_le_bytes());
}
buf
}
}
#[derive(Debug, Clone)]
pub struct WorkspaceGrid {
pub nx: usize,
pub ny: usize,
pub nz: usize,
pub cell_size: f64,
pub origin: Vec3,
pub data: Vec<f32>,
}
impl WorkspaceGrid {
pub fn new(nx: usize, ny: usize, nz: usize, cell_size: f64, origin: Vec3) -> Self {
Self {
nx,
ny,
nz,
cell_size,
origin,
data: vec![0.0; nx * ny * nz],
}
}
pub fn world_to_index(&self, pos: Vec3) -> Option<(usize, usize, usize)> {
let ix = ((pos[0] - self.origin[0]) / self.cell_size) as isize;
let iy = ((pos[1] - self.origin[1]) / self.cell_size) as isize;
let iz = ((pos[2] - self.origin[2]) / self.cell_size) as isize;
if ix < 0 || iy < 0 || iz < 0 {
return None;
}
let (ix, iy, iz) = (ix as usize, iy as usize, iz as usize);
if ix >= self.nx || iy >= self.ny || iz >= self.nz {
return None;
}
Some((ix, iy, iz))
}
pub fn get_at(&self, pos: Vec3) -> Option<f32> {
let (ix, iy, iz) = self.world_to_index(pos)?;
Some(self.data[ix + self.nx * (iy + self.ny * iz)])
}
pub fn set_at(&mut self, pos: Vec3, value: f32) -> bool {
if let Some((ix, iy, iz)) = self.world_to_index(pos) {
self.data[ix + self.nx * (iy + self.ny * iz)] = value;
true
} else {
false
}
}
pub fn reachability_fraction(&self, threshold: f32) -> f64 {
let count = self.data.iter().filter(|&&v| v >= threshold).count();
count as f64 / self.data.len() as f64
}
pub fn max_reachability(&self) -> f32 {
self.data.iter().cloned().fold(0.0f32, f32::max)
}
pub fn to_bytes(&self) -> Vec<u8> {
let header_size = 4 * 3 + 8 + 8 * 3;
let mut buf = Vec::with_capacity(header_size + self.data.len() * 4);
buf.extend_from_slice(&(self.nx as u32).to_le_bytes());
buf.extend_from_slice(&(self.ny as u32).to_le_bytes());
buf.extend_from_slice(&(self.nz as u32).to_le_bytes());
buf.extend_from_slice(&self.cell_size.to_le_bytes());
for v in &self.origin {
buf.extend_from_slice(&v.to_le_bytes());
}
for &v in &self.data {
buf.extend_from_slice(&v.to_le_bytes());
}
buf
}
}
#[derive(Debug, Clone)]
pub struct UrdfJoint {
pub name: String,
pub joint_type: JointType,
pub parent: String,
pub child: String,
pub origin_xyz: Vec3,
pub origin_rpy: Vec3,
pub axis: Vec3,
pub limits: JointLimits,
pub damping: f64,
pub friction: f64,
}
impl UrdfJoint {
pub fn revolute(
name: impl Into<String>,
parent: impl Into<String>,
child: impl Into<String>,
) -> Self {
Self {
name: name.into(),
joint_type: JointType::Revolute,
parent: parent.into(),
child: child.into(),
origin_xyz: [0.0; 3],
origin_rpy: [0.0; 3],
axis: [0.0, 0.0, 1.0],
limits: JointLimits::default(),
damping: 0.1,
friction: 0.01,
}
}
pub fn is_moveable(&self) -> bool {
!matches!(self.joint_type, JointType::Fixed)
}
pub fn local_transform_zero(&self) -> Mat4 {
let t = mat4_translation(self.origin_xyz);
let r = rpy_to_mat4(self.origin_rpy);
mat4_mul(t, r)
}
}
#[derive(Debug, Clone)]
pub struct IkSolution {
pub success: bool,
pub joint_positions: Vec<f64>,
pub position_error: f64,
pub orientation_error: f64,
pub iterations: u32,
pub solver_name: String,
}
impl IkSolution {
pub fn success(
joint_positions: Vec<f64>,
position_error: f64,
orientation_error: f64,
iterations: u32,
) -> Self {
Self {
success: true,
joint_positions,
position_error,
orientation_error,
iterations,
solver_name: "default".to_string(),
}
}
pub fn failure(iterations: u32) -> Self {
Self {
success: false,
joint_positions: Vec::new(),
position_error: f64::INFINITY,
orientation_error: f64::INFINITY,
iterations,
solver_name: "default".to_string(),
}
}
pub fn total_error(&self) -> f64 {
(self.position_error * self.position_error
+ self.orientation_error * self.orientation_error)
.sqrt()
}
pub fn to_json_string(&self) -> String {
let joints: Vec<String> = self
.joint_positions
.iter()
.map(|q| format!("{q:.6}"))
.collect();
format!(
"{{\"success\":{},\"joints\":[{}],\"pos_err\":{:.8},\"ori_err\":{:.8},\"iter\":{}}}",
self.success,
joints.join(","),
self.position_error,
self.orientation_error,
self.iterations
)
}
}
#[derive(Debug, Default, Clone)]
pub struct IkSolutionLog {
pub solutions: Vec<IkSolution>,
}
impl IkSolutionLog {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, sol: IkSolution) {
self.solutions.push(sol);
}
pub fn success_rate(&self) -> f64 {
if self.solutions.is_empty() {
return 0.0;
}
let n = self.solutions.iter().filter(|s| s.success).count();
n as f64 / self.solutions.len() as f64
}
pub fn mean_position_error(&self) -> f64 {
let succ: Vec<_> = self.solutions.iter().filter(|s| s.success).collect();
if succ.is_empty() {
return f64::NAN;
}
succ.iter().map(|s| s.position_error).sum::<f64>() / succ.len() as f64
}
pub fn mean_iterations(&self) -> f64 {
if self.solutions.is_empty() {
return 0.0;
}
self.solutions
.iter()
.map(|s| s.iterations as f64)
.sum::<f64>()
/ self.solutions.len() as f64
}
}
#[derive(Debug, Clone, Copy)]
pub struct CameraIntrinsics {
pub fx: f64,
pub fy: f64,
pub cx: f64,
pub cy: f64,
pub width: u32,
pub height: u32,
}
impl CameraIntrinsics {
pub fn new(fx: f64, fy: f64, cx: f64, cy: f64, width: u32, height: u32) -> Self {
Self {
fx,
fy,
cx,
cy,
width,
height,
}
}
pub fn fov_x(&self) -> f64 {
2.0 * (self.width as f64 / (2.0 * self.fx)).atan()
}
pub fn fov_y(&self) -> f64 {
2.0 * (self.height as f64 / (2.0 * self.fy)).atan()
}
pub fn project(&self, point: Vec3) -> Option<[f64; 2]> {
let z = point[2];
if z <= 0.0 {
return None;
}
let u = self.fx * point[0] / z + self.cx;
let v = self.fy * point[1] / z + self.cy;
Some([u, v])
}
}
#[derive(Debug, Clone, Copy)]
pub struct ScanPoint {
pub position: Vec3,
pub intensity: f32,
pub ring: u16,
pub return_num: u8,
}
impl ScanPoint {
pub fn from_position(pos: Vec3) -> Self {
Self {
position: pos,
intensity: 0.0,
ring: 0,
return_num: 1,
}
}
}
#[derive(Debug, Clone, Copy)]
pub(super) struct CubicCoeffs {
pub(super) a0: f64,
pub(super) a1: f64,
pub(super) a2: f64,
pub(super) a3: f64,
}
impl CubicCoeffs {
fn eval(&self, t: f64) -> f64 {
self.a0 + self.a1 * t + self.a2 * t * t + self.a3 * t * t * t
}
fn deriv(&self, t: f64) -> f64 {
self.a1 + 2.0 * self.a2 * t + 3.0 * self.a3 * t * t
}
}
#[derive(Debug, Clone)]
pub struct UrdfVisualElement {
pub name: Option<String>,
pub origin_xyz: Vec3,
pub origin_rpy: Vec3,
pub geometry: UrdfGeometry,
pub material: Option<String>,
}
impl UrdfVisualElement {
pub fn new(geometry: UrdfGeometry) -> Self {
Self {
name: None,
origin_xyz: [0.0; 3],
origin_rpy: [0.0; 3],
geometry,
material: None,
}
}
pub fn bounding_radius(&self) -> f64 {
match &self.geometry {
UrdfGeometry::Box { half_extents } => norm3(*half_extents),
UrdfGeometry::Sphere { radius } => *radius,
UrdfGeometry::Cylinder {
radius,
half_length,
} => (radius * radius + half_length * half_length).sqrt(),
UrdfGeometry::Mesh { scale, .. } => norm3(*scale),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct WrenchStamped {
pub timestamp: f64,
pub wrench: Wrench,
}
impl WrenchStamped {
pub fn new(timestamp: f64, wrench: Wrench) -> Self {
Self { timestamp, wrench }
}
}
#[derive(Debug, Clone)]
pub struct CameraRobotCalibration {
pub camera_to_robot: Mat4,
pub robot_to_camera: Mat4,
pub intrinsics: CameraIntrinsics,
pub residual_pixels: f64,
pub calibration_date: String,
}
impl CameraRobotCalibration {
pub fn identity(intrinsics: CameraIntrinsics) -> Self {
Self {
camera_to_robot: mat4_identity(),
robot_to_camera: mat4_identity(),
intrinsics,
residual_pixels: 0.0,
calibration_date: "2026-01-01".to_string(),
}
}
pub fn camera_to_robot_point(&self, p: Vec3) -> Vec3 {
apply_transform(&self.camera_to_robot, p)
}
pub fn robot_to_camera_point(&self, p: Vec3) -> Vec3 {
apply_transform(&self.robot_to_camera, p)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Wrench {
pub force: Vec3,
pub torque: Vec3,
}
impl Wrench {
pub fn new(force: Vec3, torque: Vec3) -> Self {
Self { force, torque }
}
pub fn force_magnitude(&self) -> f64 {
norm3(self.force)
}
pub fn torque_magnitude(&self) -> f64 {
norm3(self.torque)
}
pub fn add(&self, other: &Wrench) -> Wrench {
Wrench {
force: add3(self.force, other.force),
torque: add3(self.torque, other.torque),
}
}
pub fn scale(&self, s: f64) -> Wrench {
Wrench {
force: scale3(self.force, s),
torque: scale3(self.torque, s),
}
}
pub fn to_bytes(&self) -> [u8; 48] {
let mut buf = [0u8; 48];
for (i, v) in self.force.iter().chain(self.torque.iter()).enumerate() {
buf[i * 8..(i + 1) * 8].copy_from_slice(&v.to_le_bytes());
}
buf
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MsgType {
JointState = 1,
Wrench = 2,
GripperState = 3,
PointCloud = 4,
Generic = 255,
}
#[derive(Debug, Clone)]
pub struct UrdfInertial {
pub mass: f64,
pub com: Vec3,
pub inertia_diag: Vec3,
pub inertia_off: Vec3,
}
#[derive(Debug, Clone, PartialEq)]
pub enum UrdfGeometry {
Box {
half_extents: Vec3,
},
Sphere {
radius: f64,
},
Cylinder {
radius: f64,
half_length: f64,
},
Mesh {
filename: String,
scale: Vec3,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum JointType {
Fixed,
Revolute,
Prismatic,
Continuous,
Planar,
Floating,
}
#[derive(Debug, Clone)]
pub struct UrdfLink {
pub name: String,
pub inertial: UrdfInertial,
pub visuals: Vec<UrdfVisualElement>,
pub collisions: Vec<UrdfVisualElement>,
}
impl UrdfLink {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
inertial: UrdfInertial::default(),
visuals: Vec::new(),
collisions: Vec::new(),
}
}
pub fn approximate_visual_volume(&self) -> f64 {
self.visuals
.iter()
.map(|v| {
let r = v.bounding_radius();
(4.0 / 3.0) * std::f64::consts::PI * r * r * r
})
.sum()
}
}
#[derive(Debug, Clone)]
pub struct JointWaypoint {
pub time: f64,
pub positions: Vec<f64>,
pub velocities: Option<Vec<f64>>,
pub accelerations: Option<Vec<f64>>,
}
impl JointWaypoint {
pub fn new(time: f64, positions: Vec<f64>) -> Self {
Self {
time,
positions,
velocities: None,
accelerations: None,
}
}
}
#[derive(Debug, Clone)]
pub struct RosMsg {
pub msg_type: MsgType,
pub seq: u32,
pub payload: Vec<u8>,
}
impl RosMsg {
pub fn new(msg_type: MsgType, seq: u32, payload: Vec<u8>) -> Self {
Self {
msg_type,
seq,
payload,
}
}
fn checksum(data: &[u8]) -> u32 {
data.iter().fold(0u32, |acc, &b| acc.wrapping_add(b as u32))
}
pub fn to_bytes(&self) -> Vec<u8> {
let len = self.payload.len() as u32;
let crc = Self::checksum(&self.payload);
let mut buf = Vec::with_capacity(17 + self.payload.len());
buf.extend_from_slice(&ROS_MAGIC);
buf.push(self.msg_type as u8);
buf.extend_from_slice(&self.seq.to_le_bytes());
buf.extend_from_slice(&len.to_le_bytes());
buf.extend_from_slice(&self.payload);
buf.extend_from_slice(&crc.to_le_bytes());
buf
}
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 17 {
return None;
}
if data[0..4] != ROS_MAGIC {
return None;
}
let msg_type = MsgType::try_from(data[4]).ok()?;
let seq = u32::from_le_bytes(data[5..9].try_into().ok()?);
let len = u32::from_le_bytes(data[9..13].try_into().ok()?) as usize;
if data.len() < 13 + len + 4 {
return None;
}
let payload = data[13..13 + len].to_vec();
let crc_stored = u32::from_le_bytes(data[13 + len..17 + len].try_into().ok()?);
let crc_computed = Self::checksum(&payload);
if crc_stored != crc_computed {
return None;
}
Some(Self {
msg_type,
seq,
payload,
})
}
}
#[derive(Debug, Default, Clone)]
pub struct RobotPointCloud {
pub timestamp: f64,
pub points: Vec<ScanPoint>,
pub frame_id: String,
pub scanner_model: String,
}
impl RobotPointCloud {
pub fn new(timestamp: f64, frame_id: impl Into<String>) -> Self {
Self {
timestamp,
points: Vec::new(),
frame_id: frame_id.into(),
scanner_model: "unknown".to_string(),
}
}
pub fn len(&self) -> usize {
self.points.len()
}
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
pub fn aabb(&self) -> Option<(Vec3, Vec3)> {
if self.points.is_empty() {
return None;
}
let mut mn = [f64::INFINITY; 3];
let mut mx = [f64::NEG_INFINITY; 3];
for p in &self.points {
for k in 0..3 {
if p.position[k] < mn[k] {
mn[k] = p.position[k];
}
if p.position[k] > mx[k] {
mx[k] = p.position[k];
}
}
}
Some((mn, mx))
}
pub fn centroid(&self) -> Option<Vec3> {
if self.points.is_empty() {
return None;
}
let n = self.points.len() as f64;
let mut sum = [0.0; 3];
for p in &self.points {
sum[0] += p.position[0];
sum[1] += p.position[1];
sum[2] += p.position[2];
}
Some([sum[0] / n, sum[1] / n, sum[2] / n])
}
pub fn transform_in_place(&mut self, m: &Mat4) {
for p in &mut self.points {
p.position = apply_transform(m, p.position);
}
}
pub fn downsample(&self, stride: usize) -> Self {
let stride = stride.max(1);
let points: Vec<ScanPoint> = self
.points
.iter()
.enumerate()
.filter(|(i, _)| i % stride == 0)
.map(|(_, p)| *p)
.collect();
Self {
timestamp: self.timestamp,
points,
frame_id: self.frame_id.clone(),
scanner_model: self.scanner_model.clone(),
}
}
pub fn to_bytes(&self) -> Vec<u8> {
let n = self.points.len();
let mut buf = Vec::with_capacity(4 + n * 16);
buf.extend_from_slice(&(n as u32).to_le_bytes());
for p in &self.points {
for k in 0..3 {
buf.extend_from_slice(&(p.position[k] as f32).to_le_bytes());
}
buf.extend_from_slice(&p.intensity.to_le_bytes());
}
buf
}
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 4 {
return None;
}
let n = u32::from_le_bytes(data[0..4].try_into().ok()?) as usize;
if data.len() < 4 + n * 16 {
return None;
}
let mut points = Vec::with_capacity(n);
for i in 0..n {
let base = 4 + i * 16;
let read_f32 = |b: &[u8]| f32::from_le_bytes(b.try_into().unwrap_or([0u8; 4]));
let x = read_f32(&data[base..base + 4]) as f64;
let y = read_f32(&data[base + 4..base + 8]) as f64;
let z = read_f32(&data[base + 8..base + 12]) as f64;
let intensity = read_f32(&data[base + 12..base + 16]);
points.push(ScanPoint {
position: [x, y, z],
intensity,
ring: 0,
return_num: 1,
});
}
Some(Self {
timestamp: 0.0,
points,
frame_id: String::new(),
scanner_model: String::new(),
})
}
}
#[derive(Debug, Clone)]
pub struct JointLimits {
pub lower: f64,
pub upper: f64,
pub effort: f64,
pub velocity: f64,
}
impl JointLimits {
pub fn in_range(&self, q: f64) -> bool {
q >= self.lower && q <= self.upper
}
pub fn clamp(&self, q: f64) -> f64 {
q.clamp(self.lower, self.upper)
}
pub fn range(&self) -> f64 {
self.upper - self.lower
}
}
#[derive(Debug, Clone, Copy)]
pub struct GripperState {
pub position: f64,
pub force: f64,
pub max_width: f64,
pub object_detected: bool,
pub temperature: f64,
}
impl GripperState {
pub fn openness(&self) -> f64 {
if self.max_width < 1e-9 {
0.0
} else {
(self.position / self.max_width).clamp(0.0, 1.0)
}
}
pub fn is_closed(&self) -> bool {
self.position < 1e-3
}
pub fn to_bytes(&self) -> [u8; 40] {
let mut buf = [0u8; 40];
buf[0..8].copy_from_slice(&self.position.to_le_bytes());
buf[8..16].copy_from_slice(&self.force.to_le_bytes());
buf[16..24].copy_from_slice(&self.max_width.to_le_bytes());
buf[24..32].copy_from_slice(&self.temperature.to_le_bytes());
buf[32] = self.object_detected as u8;
buf
}
pub fn from_bytes(b: &[u8; 40]) -> Self {
let read = |s: &[u8]| f64::from_le_bytes(s.try_into().unwrap_or([0u8; 8]));
Self {
position: read(&b[0..8]),
force: read(&b[8..16]),
max_width: read(&b[16..24]),
temperature: read(&b[24..32]),
object_detected: b[32] != 0,
}
}
}
#[derive(Debug, Default, Clone)]
pub struct RobotTrajectory {
pub waypoints: Vec<JointWaypoint>,
pub joint_names: Vec<String>,
}
impl RobotTrajectory {
pub fn new(joint_names: Vec<String>) -> Self {
Self {
waypoints: Vec::new(),
joint_names,
}
}
pub fn push(&mut self, wp: JointWaypoint) {
self.waypoints.push(wp);
}
pub fn duration(&self) -> f64 {
self.waypoints.last().map(|w| w.time).unwrap_or(0.0)
}
pub fn dof(&self) -> usize {
self.waypoints
.first()
.map(|w| w.positions.len())
.unwrap_or(0)
}
pub fn interpolate(&self, t: f64) -> Option<Vec<f64>> {
let wps = &self.waypoints;
if wps.len() < 2 {
return None;
}
let dof = self.dof();
let t_clamped = t.clamp(wps[0].time, wps[wps.len() - 1].time);
let seg = wps
.windows(2)
.enumerate()
.find(|(_, pair)| t_clamped >= pair[0].time && t_clamped <= pair[1].time)
.map(|(i, _)| i)
.unwrap_or(wps.len() - 2);
let t0 = wps[seg].time;
let t1 = wps[seg + 1].time;
let dt = (t1 - t0).max(1e-12);
let u = (t_clamped - t0) / dt;
let mut result = vec![0.0; dof];
for j in 0..dof {
let p0 = wps[seg].positions[j];
let p1 = wps[seg + 1].positions[j];
let v0 = wps[seg]
.velocities
.as_ref()
.map(|v| v[j] * dt)
.unwrap_or(0.0);
let v1 = wps[seg + 1]
.velocities
.as_ref()
.map(|v| v[j] * dt)
.unwrap_or(0.0);
let c = CubicCoeffs {
a0: p0,
a1: v0,
a2: 3.0 * (p1 - p0) - 2.0 * v0 - v1,
a3: -2.0 * (p1 - p0) + v0 + v1,
};
result[j] = c.eval(u);
}
Some(result)
}
pub fn sample_uniform(&self, n_samples: usize) -> Vec<(f64, Vec<f64>)> {
if n_samples == 0 || self.waypoints.is_empty() {
return Vec::new();
}
let dur = self.duration();
(0..n_samples)
.filter_map(|i| {
let t = if n_samples == 1 {
0.0
} else {
i as f64 * dur / (n_samples - 1) as f64
};
self.interpolate(t).map(|q| (t, q))
})
.collect()
}
pub fn to_csv(&self) -> String {
let header: Vec<String> = std::iter::once("time".to_string())
.chain(self.joint_names.iter().cloned())
.collect();
let mut lines = vec![header.join(",")];
for wp in &self.waypoints {
let row: Vec<String> = std::iter::once(format!("{:.6}", wp.time))
.chain(wp.positions.iter().map(|p| format!("{:.6}", p)))
.collect();
lines.push(row.join(","));
}
lines.join("\n")
}
}
#[derive(Debug, Default, Clone)]
pub struct UrdfRobot {
pub name: String,
pub links: HashMap<String, UrdfLink>,
pub joints: HashMap<String, UrdfJoint>,
pub joint_order: Vec<String>,
}
impl UrdfRobot {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
..Default::default()
}
}
pub fn add_link(&mut self, link: UrdfLink) {
self.links.insert(link.name.clone(), link);
}
pub fn add_joint(&mut self, joint: UrdfJoint) {
if !self.joint_order.contains(&joint.name) {
self.joint_order.push(joint.name.clone());
}
self.joints.insert(joint.name.clone(), joint);
}
pub fn moveable_joint_names(&self) -> Vec<&str> {
self.joint_order
.iter()
.filter(|n| {
self.joints
.get(n.as_str())
.map(|j| j.is_moveable())
.unwrap_or(false)
})
.map(|s| s.as_str())
.collect()
}
pub fn dof(&self) -> usize {
self.moveable_joint_names().len()
}
pub fn total_mass(&self) -> f64 {
self.links.values().map(|l| l.inertial.mass).sum()
}
pub fn to_text(&self) -> String {
let mut out = format!("robot name=\"{}\"\n", self.name);
for (name, link) in &self.links {
out += &format!(" link \"{name}\" mass={:.4} kg\n", link.inertial.mass);
}
for name in &self.joint_order {
if let Some(j) = self.joints.get(name) {
out += &format!(
" joint \"{}\" type={} parent=\"{}\" child=\"{}\"\n",
j.name, j.joint_type, j.parent, j.child
);
}
}
out
}
pub fn from_text(text: &str) -> Option<Self> {
let mut robot = UrdfRobot::default();
for line in text.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("robot name=\"") {
robot.name = rest.trim_end_matches('"').to_string();
} else if let Some(rest) = line.strip_prefix("link \"") {
let parts: Vec<&str> = rest.splitn(2, '"').collect();
if let Some(name) = parts.first() {
let mass_str = rest
.find("mass=")
.map(|i| &rest[i + 5..])
.and_then(|s| s.split_whitespace().next())
.and_then(|s| s.trim_end_matches(" kg").parse::<f64>().ok())
.unwrap_or(1.0);
let mut link = UrdfLink::new(*name);
link.inertial.mass = mass_str;
robot.add_link(link);
}
}
}
if robot.name.is_empty() {
None
} else {
Some(robot)
}
}
}