1#[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
80pub const URDF_FILE: &str = "robot.urdf";
82
83#[derive(Clone, Copy, Debug, PartialEq)]
87pub struct Origin {
88 pub xyz: [f64; 3],
89 pub rpy: [f64; 3],
90}
91
92impl Origin {
93 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#[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#[derive(Clone, Debug, PartialEq)]
115pub struct Visual {
116 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 pub parts: Vec<String>,
127 pub inertial: Option<Inertial>,
129 pub visuals: Vec<Visual>,
130}
131
132#[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#[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 pub name: String,
162 pub kind: JointType,
163 pub parent: String,
164 pub child: String,
165 pub origin: Origin,
167 pub axis: [f64; 3],
169 pub mimic: Option<Mimic>,
170}
171
172#[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#[derive(Clone, Debug)]
193pub struct UrdfExport {
194 pub robot: Robot,
195 pub meshes: Vec<(String, Vec<u8>)>,
197}
198
199impl UrdfExport {
200 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 pub fn zip(&self) -> GeopResult<Vec<u8>> {
210 zip::zip(&self.files())
211 }
212}
213
214pub struct Urdf;
217
218#[derive(Clone, Debug, PartialEq, Deserialize)]
220#[serde(default)]
221pub struct UrdfOptions {
222 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
254fn 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
267pub struct Body<'a> {
270 pub name: String,
272 pub free: bool,
275 pub carrier: Option<usize>,
279 pub world: Pose,
281 pub part: Reader<'a>,
283 pub file: String,
285}
286
287pub struct End {
290 pub body: Option<usize>,
291 pub connector: Connector,
292}
293
294pub 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
305pub struct Coupling {
308 pub name: String,
309 pub kind: CouplingKind,
310 pub a: usize,
311 pub b: usize,
312}
313
314pub 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 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 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 _ => 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
452fn 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
459fn 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
474fn turn(degrees: Scalar) -> GeopResult<Pose> {
476 let radians = degrees.degrees_to_radians()?;
477 Pose::rotation_about(&Vector3::zero(), &Vector3::axis(2), radians)
478}
479
480fn slide(distance: Scalar) -> Pose {
482 Pose::identity().with_position(Vector3::from_array([Scalar::ZERO, Scalar::ZERO, distance]))
483}
484
485struct Meshes {
489 paths: HashMap<Option<String>, Option<String>>,
490 files: Vec<(String, Vec<u8>)>,
491 quality: usize,
492}
493
494impl Meshes {
495 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 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
532fn 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
549fn 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
569pub 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 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 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 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 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 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 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
727fn mimics(mechanism: &Mechanism, tree: &tree::Tree, joints: &mut [Joint]) -> GeopResult<()> {
733 let couplings = &mechanism.couplings;
734 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 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}