Skip to main content

geop_ops_assembly/urdf/
mod.rs

1//! URDF export: an assembly as a robot description, for simulators — ROS,
2//! Gazebo, MuJoCo, PyBullet, Isaac — to load ([`export`]), written by the
3//! exporter [`Urdf`] as one ZIP archive of the robot and its meshes.
4//!
5//! A robot is a tree of **links** joined by **joints**. Its links are the
6//! placed parts, those that mates hold rigidly together merged into one,
7//! the root the fixed part (see [`tree`]); its joints are the assembly's
8//! revolute joints (`revolute` with limits, `continuous` without) and
9//! sliders (`prismatic`). A coupling between two of them — gears, a rack
10//! and pinion, a screw — makes the second **mimic** the first.
11//!
12//! **Frames.** The root link's frame is the assembly's. Every other link's
13//! frame is its joint's: origin on the axis, `z` along it, `x` the
14//! direction turns are measured from — so every joint's axis is `z`, and a
15//! joint's coordinate in the URDF is the assembly's own: at the angle the
16//! assembly measures, the link is where it is drawn, and the limits are the
17//! joint's limits. Joint origins come from the connectors where the
18//! assembly is now; the exported pose is that of zero coordinates.
19//!
20//! **Units.** URDF is SI: lengths in metres, angles in radians, masses in
21//! kilograms, inertia in kg·m². geop's lengths are millimetres, so meshes
22//! are written in millimetres and scaled by `0.001`.
23//!
24//! **Inertia.** Each link's mass, centre of mass and inertia tensor come
25//! from the exact B-rep of its solids, each of its own part's material
26//! (see [`geop_ops_model::placed::solids`]), the tensor about the centre of mass
27//! along the link frame's axes. A link of no material at all has no
28//! `<inertial>`; one of which only some solids have a material is refused,
29//! naming a solid of none: its mass would leave them out.
30//!
31//! **Meshes.** Each placed component is meshed once, in its own frame, as
32//! binary STL (`meshes/<file>.stl`), and is each link's `<visual>` and
33//! `<collision>` where its parts are. geop models no actuators: `effort`
34//! and `velocity`, which URDF requires of a limited joint, are written as
35//! 0, as CAD exporters do — set them for the drives.
36//!
37//! What URDF cannot express is refused, by name: closed loops (a four-bar),
38//! parts that mates leave free to move but no joint names, cylindrical
39//! joints, limits one way only.
40
41#[cfg(test)]
42mod tests;
43pub mod tree;
44mod xml;
45pub mod zip;
46
47use geop_core_program::{reader::Reader, values::StateValue};
48use std::collections::{BTreeMap, BTreeSet, HashMap};
49
50use crate::{
51    CouplingKind, JointKind, MOTIONS, Motion, Resolved, joint_parameter,
52    joints::{Connector, measure},
53    mechanism::mates_of,
54    residuals::D,
55    solving::{self, check},
56};
57use geop_core_editor::{
58    editor::Rerun,
59    output::{Content, Export, Exporter, ExporterInfo, Output},
60};
61use geop_core_math::{
62    geop_error::{GeopError, GeopResult, WithContext},
63    primitives::{Pose, Quaternion},
64    scalars::Scalar,
65    solvers::walk::{Walked, walk},
66    vector::Vector3,
67};
68use geop_core_program::placed::{instance_path, split_instance_path};
69use geop_core_rasterize::stl::StlFormat;
70use geop_core_topology::mass::MassProperties;
71use geop_ops_model::parameters::{Placement, stem};
72use geop_ops_model::placed::{
73    drawn, placed_parts,
74    solids::{Shapes, own_solids_at},
75};
76use geop_ops_model::read_model;
77use geop_ops_model::{STL_QUALITY, stl_bytes};
78use serde::Deserialize;
79
80/// The URDF file's path in an export.
81pub const URDF_FILE: &str = "robot.urdf";
82
83/// A frame relative to another, as URDF writes it: moved by `xyz` (metres)
84/// after turning by `rpy` (radians) — about `x`, then `y`, then `z`, the
85/// fixed axes.
86#[derive(Clone, Copy, Debug, PartialEq)]
87pub struct Origin {
88    pub xyz: [f64; 3],
89    pub rpy: [f64; 3],
90}
91
92impl Origin {
93    /// `pose`, a geop pose in millimetres.
94    fn of(pose: &Pose) -> Self {
95        let p = pose.position();
96        Origin {
97            xyz: [0, 1, 2].map(|k| p[k].to_f64() / 1000.0),
98            rpy: pose.euler_degrees().map(f64::to_radians),
99        }
100    }
101}
102
103/// A link's mass (kg), centre of mass in its frame (m), and inertia tensor
104/// about the centre along its frame's axes (kg·m²).
105#[derive(Clone, Copy, Debug, PartialEq)]
106pub struct Inertial {
107    pub mass: f64,
108    pub center: [f64; 3],
109    pub inertia: [[f64; 3]; 3],
110}
111
112/// One placed part of a link, drawn: its mesh, a path in the export, where
113/// it is in the link's frame.
114#[derive(Clone, Debug, PartialEq)]
115pub struct Visual {
116    /// The placed part, by name — or `base` for the assembly's own solids.
117    pub name: String,
118    pub origin: Origin,
119    pub mesh: String,
120}
121
122#[derive(Clone, Debug, PartialEq)]
123pub struct Link {
124    pub name: String,
125    /// The placed parts it is made of, by name.
126    pub parts: Vec<String>,
127    /// None for a link without solids.
128    pub inertial: Option<Inertial>,
129    pub visuals: Vec<Visual>,
130}
131
132/// What a joint lets its child do, and within what limits: radians for a
133/// turn, metres for a slide.
134#[derive(Clone, Copy, Debug, PartialEq)]
135pub enum JointType {
136    Revolute { lower: f64, upper: f64 },
137    Continuous,
138    Prismatic { lower: f64, upper: f64 },
139}
140
141impl JointType {
142    pub fn name(&self) -> &'static str {
143        match self {
144            JointType::Revolute { .. } => "revolute",
145            JointType::Continuous => "continuous",
146            JointType::Prismatic { .. } => "prismatic",
147        }
148    }
149}
150
151/// A joint's coordinate following another's: `multiplier` times it.
152#[derive(Clone, Debug, PartialEq)]
153pub struct Mimic {
154    pub joint: String,
155    pub multiplier: f64,
156}
157
158#[derive(Clone, Debug, PartialEq)]
159pub struct Joint {
160    /// The joint mate's name in the assembly.
161    pub name: String,
162    pub kind: JointType,
163    pub parent: String,
164    pub child: String,
165    /// The child link's frame, at a coordinate of zero, in the parent's.
166    pub origin: Origin,
167    /// Along the child's `z`, either way.
168    pub axis: [f64; 3],
169    pub mimic: Option<Mimic>,
170}
171
172/// A robot: its links, the root first, and its joints, each after the
173/// joint carrying its parent.
174#[derive(Clone, Debug, PartialEq)]
175pub struct Robot {
176    pub name: String,
177    pub links: Vec<Link>,
178    pub joints: Vec<Joint>,
179}
180
181impl Robot {
182    pub fn link(&self, name: &str) -> Option<&Link> {
183        self.links.iter().find(|l| l.name == name)
184    }
185
186    pub fn joint(&self, name: &str) -> Option<&Joint> {
187        self.joints.iter().find(|j| j.name == name)
188    }
189}
190
191/// A robot and the files it is written as.
192#[derive(Clone, Debug)]
193pub struct UrdfExport {
194    pub robot: Robot,
195    /// The meshes, by path: `meshes/link.stl`.
196    pub meshes: Vec<(String, Vec<u8>)>,
197}
198
199impl UrdfExport {
200    /// Every file: the URDF ([`URDF_FILE`]) first, then the meshes, by
201    /// paths relative to it.
202    pub fn files(&self) -> Vec<(String, Vec<u8>)> {
203        let mut files = vec![(URDF_FILE.to_string(), self.robot.to_urdf().into_bytes())];
204        files.extend(self.meshes.iter().cloned());
205        files
206    }
207
208    /// Every file in one ZIP archive.
209    pub fn zip(&self) -> GeopResult<Vec<u8>> {
210        zip::zip(&self.files())
211    }
212}
213
214/// Writes an assembly as a URDF robot: one ZIP archive of `robot.urdf` and
215/// its meshes (see [`export`]).
216pub struct Urdf;
217
218/// How a URDF robot is written.
219#[derive(Clone, Debug, PartialEq, Deserialize)]
220#[serde(default)]
221pub struct UrdfOptions {
222    /// How finely curved faces are meshed: higher is smoother, and bigger.
223    pub quality: usize,
224}
225
226impl Default for UrdfOptions {
227    fn default() -> Self {
228        Self {
229            quality: STL_QUALITY,
230        }
231    }
232}
233
234impl Exporter for Urdf {
235    const INFO: ExporterInfo = ExporterInfo {
236        kind: "urdf",
237        label: "URDF",
238        doc: "robot, for simulators",
239        formats: &["zip"],
240    };
241    type Options = UrdfOptions;
242
243    fn export(output: &mut Output, _: &str, options: UrdfOptions) -> GeopResult<Export> {
244        let name = stem(output.file_name()).to_string();
245        let robot = export(output, &name, options.quality)?;
246        Ok(Export {
247            name: format!("{name}.zip"),
248            media_type: "application/zip".into(),
249            content: Content::Bytes(robot.zip()?),
250        })
251    }
252}
253
254/// `name` as a file name: letters, digits, `-` and `_`, every other
255/// character `_`.
256fn file_name(name: &str) -> String {
257    let name: String = name
258        .chars()
259        .map(|c| match c {
260            'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => c,
261            _ => '_',
262        })
263        .collect();
264    if name.is_empty() { "part".into() } else { name }
265}
266
267/// A rigid body of the mechanism: a part placed in the assembly, however
268/// deep.
269pub struct Body<'a> {
270    /// Its name in the assembly: `hinge/pin`.
271    pub name: String,
272    /// Whether the mates may move it: it has a pose of its own to solve
273    /// for, and no fixed mate holds it.
274    pub free: bool,
275    /// The body it is carried by, by index: for a copy of a pattern, the
276    /// part it copies — it goes where the pattern puts it after that part,
277    /// and moves with it as one.
278    pub carrier: Option<usize>,
279    /// Where it is in the assembly.
280    pub world: Pose,
281    /// Its part, as placed.
282    pub part: Reader<'a>,
283    /// The file its part is placed from.
284    pub file: String,
285}
286
287/// An end of a joint: the body it is on — none for the assembly itself —
288/// and its connector, in that body's frame.
289pub struct End {
290    pub body: Option<usize>,
291    pub connector: Connector,
292}
293
294/// A joint of the mechanism, at the coordinates the state has: the angle in
295/// degrees, and the distance.
296pub struct MechanismJoint {
297    pub name: String,
298    pub kind: JointKind,
299    pub a: End,
300    pub b: End,
301    pub angle: Scalar,
302    pub distance: Scalar,
303}
304
305/// A coupling of the mechanism: its second joint's coordinate following
306/// its first's, by index in [`Mechanism::joints`].
307pub struct Coupling {
308    pub name: String,
309    pub kind: CouplingKind,
310    pub a: usize,
311    pub b: usize,
312}
313
314/// The rigid bodies of an assembly and its mates between them, as the
315/// part a reader reads has them: the bodies in the order they were placed,
316/// the constraints — by name, and the bodies of their two entities — the
317/// joints and the couplings each in the order the steps made them.
318pub struct Mechanism<'a> {
319    pub bodies: Vec<Body<'a>>,
320    pub constraints: Vec<(String, [Option<usize>; 2])>,
321    pub joints: Vec<MechanismJoint>,
322    pub couplings: Vec<Coupling>,
323}
324
325impl<'a> Mechanism<'a> {
326    /// The mechanism of the part `reader` reads, its joints at the
327    /// coordinates the state has them — or, where it has none, where their
328    /// parts are.
329    pub fn of(reader: &Reader<'a>) -> GeopResult<Self> {
330        let placed = placed_parts(reader)?;
331        let resolved = mates_of(reader);
332        let fixed: BTreeSet<&str> = (resolved.iter())
333            .filter_map(|(_, r)| match r {
334                Resolved::Fixed { body } => Some(body.as_str()),
335                _ => None,
336            })
337            .collect();
338        let index: HashMap<String, usize> = (placed.iter().enumerate())
339            .map(|(i, p)| (p.name.clone(), i))
340            .collect();
341        let bodies: Vec<Body> = (placed.into_iter())
342            .map(|p| {
343                // A copy is placed after the part it copies, beside it.
344                let carrier = match &p.located.instance.placement {
345                    Placement::After { of, .. } => {
346                        let (outer, _) = split_instance_path(&p.name);
347                        index.get(&instance_path(outer, of)).copied()
348                    }
349                    Placement::State(_) => None,
350                };
351                Body {
352                    free: p.located.state.is_some() && !fixed.contains(p.name.as_str()),
353                    carrier,
354                    world: p.located.world,
355                    file: p.located.instance.file.clone(),
356                    part: p.located.part,
357                    name: p.name,
358                }
359            })
360            .collect();
361        let body = |name: &Option<String>| -> GeopResult<Option<usize>> {
362            name.as_ref()
363                .map(|n| {
364                    (index.get(n.as_str()).copied())
365                        .ok_or_else(|| GeopError::new(format!("no part is placed as {n:?}")))
366                })
367                .transpose()
368        };
369        let mut constraints = Vec::new();
370        let mut joints = Vec::new();
371        let mut coupled = Vec::new();
372        for (name, mate) in &resolved {
373            let ctx = |e: GeopError| e.with_context(format!("the mate {name:?}"));
374            match mate {
375                Resolved::Constraint { a, b, .. } => constraints.push((
376                    name.to_string(),
377                    [
378                        body(&a.body).with_context(&ctx)?,
379                        body(&b.body).with_context(&ctx)?,
380                    ],
381                )),
382                Resolved::Joint { kind, a, b } => {
383                    let (a, b) = (
384                        End {
385                            body: body(&a.body).with_context(&ctx)?,
386                            connector: a.what,
387                        },
388                        End {
389                            body: body(&b.body).with_context(&ctx)?,
390                            connector: b.what,
391                        },
392                    );
393                    let world = |end: &End| end.body.map(|b| bodies[b].world);
394                    let measured = measure(
395                        &end_world(&a.connector, world(&a).as_ref()),
396                        &end_world(&b.connector, world(&b).as_ref()),
397                    );
398                    let mut coordinates = [Scalar::ZERO; MOTIONS];
399                    for motion in kind.motions() {
400                        let k = motion.index();
401                        coordinates[k] = match reader.retrieve_state(&joint_parameter(name, motion))
402                        {
403                            StateValue::Number(value) => value,
404                            // Where its parts are: what a solve would
405                            // start it from.
406                            _ => Scalar::from_f64(measured[k]),
407                        };
408                    }
409                    joints.push(MechanismJoint {
410                        name: name.to_string(),
411                        kind: *kind,
412                        a,
413                        b,
414                        angle: coordinates[Motion::Turn.index()],
415                        distance: coordinates[Motion::Slide.index()],
416                    });
417                }
418                Resolved::Coupling { kind, joints } => {
419                    coupled.push((name.to_string(), *kind, joints.clone()));
420                }
421                Resolved::Fixed { .. } => {}
422            }
423        }
424        let couplings = (coupled.into_iter())
425            .map(|(name, kind, [a, b])| {
426                let find = |joint: &str| {
427                    (joints.iter().position(|j: &MechanismJoint| j.name == joint)).ok_or_else(
428                        || {
429                            GeopError::new(format!(
430                                "the coupling {name:?} ties the joint {joint:?}, which there is none of"
431                            ))
432                        },
433                    )
434                };
435                Ok(Coupling {
436                    a: find(&a)?,
437                    b: find(&b)?,
438                    name,
439                    kind,
440                })
441            })
442            .collect::<GeopResult<_>>()?;
443        Ok(Mechanism {
444            bodies,
445            constraints,
446            joints,
447            couplings,
448        })
449    }
450}
451
452/// `connector` where `pose` puts it — or, with none, where it is — in
453/// dual numbers, as [`measure`] takes it.
454fn end_world(connector: &Connector, pose: Option<&Pose>) -> Connector<D> {
455    let pose = pose.map(|p| p.map(D::cst));
456    connector.placed(pose.as_ref())
457}
458
459/// The frame of a joint's end `end`, where its body is: origin on the
460/// axis, `z` along it, `x` the reference turns are measured from.
461fn frame(end: &End, bodies: &[Body]) -> GeopResult<Pose> {
462    let c = &end.connector;
463    let y = c.axis.prod_cross(&c.reference);
464    let local = Pose::new(
465        c.origin,
466        Quaternion::from_rotation_columns([c.reference, y, c.axis])?,
467    )?;
468    Ok(match end.body {
469        Some(b) => bodies[b].world.compose(&local),
470        None => local,
471    })
472}
473
474/// A turn by `degrees` about `z`.
475fn turn(degrees: Scalar) -> GeopResult<Pose> {
476    let radians = degrees.degrees_to_radians()?;
477    Pose::rotation_about(&Vector3::zero(), &Vector3::axis(2), radians)
478}
479
480/// A slide by `distance` along `z`.
481fn slide(distance: Scalar) -> Pose {
482    Pose::identity().with_position(Vector3::from_array([Scalar::ZERO, Scalar::ZERO, distance]))
483}
484
485/// The meshes of an export, each written once: per placed part, by its
486/// part's id — none for the assembly's own solids — the mesh's path, if it
487/// has any triangles.
488struct Meshes {
489    paths: HashMap<Option<String>, Option<String>>,
490    files: Vec<(String, Vec<u8>)>,
491    quality: usize,
492}
493
494impl Meshes {
495    /// The path of the mesh of the solids of the part `reader` reads itself —
496    /// not of the parts placed in it, which are bodies of their own — in its
497    /// own frame, called `stem`, written now if it was not yet; `None` if it
498    /// has no solids. `key` names the part.
499    fn of(
500        &mut self,
501        key: Option<String>,
502        stem: &str,
503        reader: &Reader,
504    ) -> GeopResult<Option<String>> {
505        if let Some(path) = self.paths.get(&key) {
506            return Ok(path.clone());
507        }
508        let meshes = read_model(reader)?.solid_meshes(self.quality)?;
509        let triangles: Vec<_> = meshes.iter().flat_map(|(_, triangles)| triangles).collect();
510        let path = if triangles.is_empty() {
511            None
512        } else {
513            let stem = file_name(stem);
514            // The first name no file has: of one more names than there
515            // are files, one is free.
516            let path = (1..=self.files.len() + 1)
517                .map(|n| match n {
518                    1 => format!("meshes/{stem}.stl"),
519                    n => format!("meshes/{stem}_{n}.stl"),
520                })
521                .find(|path| self.files.iter().all(|(p, _)| p != path))
522                .expect("one of more names than files is free");
523            let bytes = stl_bytes(triangles, &stem, StlFormat::Binary)?;
524            self.files.push((path.clone(), bytes));
525            Some(path)
526        };
527        self.paths.insert(key, path.clone());
528        Ok(path)
529    }
530}
531
532/// The mass properties of the solids of the part `reader` reads itself,
533/// of the part placed at `instance`, where `pose` puts them, each of its part's
534/// material (see [`own_solids_at`]) — none for a solid of no material —
535/// each with its name.
536fn masses(
537    reader: &Reader,
538    instance: &str,
539    pose: Option<Pose>,
540    shapes: &mut Shapes,
541    out: &mut Vec<(String, Option<MassProperties>)>,
542) -> GeopResult<()> {
543    for solid in own_solids_at(reader, instance, pose)? {
544        out.push((solid.name.clone(), solid.mass_properties(shapes)?));
545    }
546    Ok(())
547}
548
549/// What the solids `masses` of the link `link` weigh together: none if
550/// none has a material. Refuses a link of which only some do, naming one
551/// that has none.
552fn link_mass(
553    link: &str,
554    masses: &[(String, Option<MassProperties>)],
555) -> GeopResult<Option<MassProperties>> {
556    let weighed: Vec<MassProperties> = masses.iter().filter_map(|(_, m)| *m).collect();
557    if weighed.is_empty() {
558        return Ok(None);
559    }
560    if let Some((unweighed, _)) = masses.iter().find(|(_, m)| m.is_none()) {
561        return Err(GeopError::new(format!(
562            "the link {link:?} has solids of a material and {unweighed:?} of none: \
563             give its part a material under Properties, or none of the link's parts one"
564        )));
565    }
566    MassProperties::combine(&weighed)
567}
568
569/// The assembly `output` built — where its parts are now — as a URDF robot
570/// called `name`, meshed `quality` fine (see
571/// [`geop_core_rasterize::rasterize`]). Refuses an assembly whose mates do
572/// not hold where its parts are — it would describe a robot that is not the
573/// one drawn — and one URDF cannot express (see [`tree`]).
574pub fn export(output: &mut Output, name: &str, quality: usize) -> GeopResult<UrdfExport> {
575    let ctx = |e: GeopError| e.with_context(format!("exporting {name:?} as URDF"));
576    let report = check(&output.part()).with_context(&ctx)?;
577    if !report.converged {
578        return Err(ctx(GeopError::new(format!(
579            "the mates {} do not hold where the parts are: solve the assembly before exporting it",
580            report.failed.join(", ")
581        ))));
582    }
583    // The tree, and every joint's coordinates: what is held to ask what
584    // mates alone leave free to move.
585    let (tree, held) = {
586        let mechanism = Mechanism::of(&output.part()).with_context(&ctx)?;
587        let held: BTreeSet<String> = (mechanism.joints.iter())
588            .flat_map(|j| (j.kind.motions().into_iter()).map(|m| joint_parameter(&j.name, m)))
589            .collect();
590        (tree::tree(&mechanism).with_context(&ctx)?, held)
591    };
592    let mut rerun = Rerun {
593        runner: output.runner,
594        part: &mut *output.part,
595        program: output.file,
596    };
597    let freedom = solving::freedom(&mut rerun, &output.file.state, &held).with_context(&ctx)?;
598    let reader = output.part();
599    let mechanism = Mechanism::of(&reader).with_context(&ctx)?;
600    tree::check_rigid(&mechanism, &tree, &freedom.parts).with_context(&ctx)?;
601    let bodies = &mechanism.bodies;
602
603    // Each link's frame in the assembly, and each joint, in tree order.
604    let mut frames = vec![Pose::identity(); tree.links.len()];
605    let mut joints = Vec::new();
606    for tj in &tree.joints {
607        let joint = &mechanism.joints[tj.joint];
608        let jctx = |e: GeopError| e.with_context(format!("the joint {:?}", joint.name));
609        let start = frame(&joint.a, bodies).with_context(&jctx)?;
610        // What of the joint's motion stays — a revolute joint's offset
611        // along its axis, a slider's turn about it — and what moves.
612        let (fixed, moving, motion) = match joint.kind.joint {
613            crate::Joint::Revolute => (slide(joint.distance), turn(joint.angle)?, Motion::Turn),
614            crate::Joint::Slider => (turn(joint.angle)?, slide(joint.distance), Motion::Slide),
615            _ => unreachable!("tree refuses every other moving joint"),
616        };
617        let at_zero = start.compose(&fixed);
618        // The child is carried by the joint from its frame at zero: on the
619        // second end's body if the joint runs forward, else on the first's,
620        // turning the other way.
621        let (origin, child, axis) = if tj.forward {
622            (at_zero, at_zero.compose(&moving), [0.0, 0.0, 1.0])
623        } else {
624            (at_zero.compose(&moving), at_zero, [0.0, 0.0, -1.0])
625        };
626        frames[tj.child] = child;
627        let [min, max] = joint.kind.limits(motion).map(|l| l.map(|v| v.to_f64()));
628        let kind = match (motion, min, max) {
629            (Motion::Turn, Some(lower), Some(upper)) => JointType::Revolute {
630                lower: lower.to_radians(),
631                upper: upper.to_radians(),
632            },
633            (Motion::Turn, _, _) => JointType::Continuous,
634            (Motion::Slide, Some(lower), Some(upper)) => JointType::Prismatic {
635                lower: lower / 1000.0,
636                upper: upper / 1000.0,
637            },
638            _ => unreachable!("tree refuses a slider without limits"),
639        };
640        joints.push(Joint {
641            name: joint.name.clone(),
642            kind,
643            parent: tree.links[tj.parent].name.clone(),
644            child: tree.links[tj.child].name.clone(),
645            origin: Origin::of(&frames[tj.parent].inverse().compose(&origin)),
646            axis,
647            mimic: None,
648        });
649    }
650    mimics(&mechanism, &tree, &mut joints).with_context(&ctx)?;
651
652    let mut meshes = Meshes {
653        paths: HashMap::new(),
654        files: Vec::new(),
655        quality,
656    };
657    let mut shapes = Shapes::default();
658    let mut links = Vec::new();
659    for (index, link) in tree.links.iter().enumerate() {
660        let back = frames[index].inverse();
661        let mut masses_of = Vec::new();
662        let mut visuals = Vec::new();
663        // The assembly's own solids are the ground's: the root's.
664        if index == 0 {
665            masses(&reader, "", None, &mut shapes, &mut masses_of).with_context(&ctx)?;
666            if let Some(mesh) = meshes.of(None, "base", &reader)? {
667                visuals.push(Visual {
668                    name: "base".into(),
669                    origin: Origin::of(&back),
670                    mesh,
671                });
672            }
673        }
674        for &b in &link.bodies {
675            let body = &bodies[b];
676            // A part placed is a body of its own solids: the parts placed
677            // in it are bodies of their own.
678            let placed = body.part.clone();
679            masses(
680                &placed,
681                &body.name,
682                Some(body.world),
683                &mut shapes,
684                &mut masses_of,
685            )
686            .with_context(&ctx)?;
687            let key = Some(drawn(&reader, &body.name).with_context(&ctx)?.key.clone());
688            let stem = stem(&body.file).to_string();
689            if let Some(mesh) = meshes.of(key, &stem, &placed)? {
690                visuals.push(Visual {
691                    name: body.name.clone(),
692                    origin: Origin::of(&back.compose(&body.world)),
693                    mesh,
694                });
695            }
696        }
697        let inertial = match link_mass(&link.name, &masses_of).with_context(&ctx)? {
698            Some(total) => {
699                let local = total.placed(&back)?;
700                Some(Inertial {
701                    mass: local.mass.to_f64(),
702                    center: [0, 1, 2].map(|k| local.center[k].to_f64() / 1000.0),
703                    inertia: local.inertia.map(|row| row.map(|v| v.to_f64() * 1e-6)),
704                })
705            }
706            None => None,
707        };
708        links.push(Link {
709            name: link.name.clone(),
710            parts: (link.bodies.iter())
711                .map(|&b| bodies[b].name.clone())
712                .collect(),
713            inertial,
714            visuals,
715        });
716    }
717    Ok(UrdfExport {
718        robot: Robot {
719            name: name.to_string(),
720            links,
721            joints,
722        },
723        meshes: meshes.files,
724    })
725}
726
727/// Makes every joint a coupling drives mimic its driver: the coupling's
728/// factor, in URDF's units — radians and metres. A joint driven through a
729/// chain of couplings mimics the first driver, the factors multiplied, as
730/// URDF readers expect. Refuses a joint two couplings drive, and couplings
731/// driving each other round in a circle.
732fn mimics(mechanism: &Mechanism, tree: &tree::Tree, joints: &mut [Joint]) -> GeopResult<()> {
733    let couplings = &mechanism.couplings;
734    // Per driven joint (by index in the mechanism), its driver, the
735    // factor, and the coupling — by index, so that a circle of couplings is
736    // reported from the same joint on every run.
737    let mut drives: BTreeMap<usize, (usize, f64, usize)> = BTreeMap::new();
738    for (i, coupling) in couplings.iter().enumerate() {
739        let [ma, mb] = coupling.kind.transmission.motions();
740        let unit = |m: Motion| match m {
741            Motion::Turn => 1.0,
742            Motion::Slide | Motion::SlideX | Motion::SlideY => 1e-3,
743        };
744        let multiplier = coupling.kind.factor()?.to_f64() * unit(mb) / unit(ma);
745        if let Some((_, _, other)) = drives.insert(coupling.b, (coupling.a, multiplier, i)) {
746            return Err(GeopError::new(format!(
747                "the joint {:?} is driven by two couplings, {} and {}, and a URDF joint mimics one other only",
748                mechanism.joints[coupling.b].name, couplings[other].name, coupling.name
749            )));
750        }
751    }
752    let index: HashMap<usize, usize> = tree
753        .joints
754        .iter()
755        .enumerate()
756        .map(|(k, tj)| (tj.joint, k))
757        .collect();
758    for (&driven, &(_, factor, _)) in &drives {
759        // From the joint driven to each joint driving it, to the one
760        // nothing drives.
761        let through = walk(driven, |j| {
762            Ok::<_, GeopError>(drives.get(j).map(|&(next, _, _)| next))
763        })?;
764        if through.end != Walked::Open {
765            let names: Vec<&str> = (through.nodes.iter())
766                .map(|&j| mechanism.joints[j].name.as_str())
767                .collect();
768            return Err(GeopError::new(format!(
769                "the couplings of the joints {} drive each other round in a circle, which URDF's mimic joints cannot express",
770                names.join(", ")
771            )));
772        }
773        let (&driver, between) = through.nodes[1..]
774            .split_last()
775            .expect("driven by one joint at least");
776        let multiplier = (between.iter()).fold(factor, |m, j| m * drives[j].1);
777        joints[index[&driven]].mimic = Some(Mimic {
778            joint: mechanism.joints[driver].name.clone(),
779            multiplier,
780        });
781    }
782    Ok(())
783}