1#![allow(clippy::too_many_arguments)] use crate::errors::ParseError;
29use collider_rs::mesh::Mesh;
30use collider_rs::shape::Cuboid;
31use collider_rs::shape::{Cylinder, ShapeWrapper, Sphere};
32use dynamics_inertia::inertia::Inertia;
33use dynamics_joint::continuous::JointModelContinuous;
34use dynamics_joint::joint::JointWrapper;
35use dynamics_joint::prismatic::JointModelPrismatic;
36use dynamics_joint::revolute::JointModelRevolute;
37use dynamics_model::frame::{Frame, FrameType};
38use dynamics_model::model::Model;
39use dynamics_model::{
40 geometry_model::GeometryModel, geometry_object::GeometryObject, model::WORLD_ID,
41};
42use dynamics_spatial::color::Color;
43use dynamics_spatial::motion::SpatialRotation;
44use dynamics_spatial::se3::{ActSE3, SE3};
45use dynamics_spatial::symmetric3::Symmetric3;
46use dynamics_spatial::vector3d::Vector3D;
47use nalgebra::Vector3;
48use roxmltree::{Document, Node};
49use std::collections::HashSet;
50use std::path::Path;
51use std::{collections::HashMap, fs, str::FromStr};
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54enum GeometryType {
56 Collision,
57 Visual,
58}
59
60pub fn build_models_from_urdf(
73 filepath: &str,
74 package_dir: Option<&str>,
75) -> Result<(Model, GeometryModel, GeometryModel), ParseError> {
76 let contents =
77 fs::read_to_string(filepath).map_err(|e| ParseError::IoError(e, filepath.to_string()))?;
78 let doc = Document::parse(&contents).map_err(ParseError::XmlError)?;
79
80 let robot_node = doc
82 .descendants()
83 .find(|n| n.tag_name().name() == "robot")
84 .ok_or(ParseError::NoRobotTag)?;
85 let robot_name = robot_node.attribute("name").unwrap_or("").to_string();
86
87 let mut model = Model::new(robot_name);
89 let mut coll_model = GeometryModel::new();
90 let mut viz_model = GeometryModel::new();
91
92 let mut materials = parse_root_materials(&robot_node)?;
94
95 let root_nodes = find_root_nodes(&robot_node)?;
97
98 let mut root_nodes: Vec<Node> = root_nodes.into_iter().collect();
100 root_nodes.sort_by_key(|n| n.attribute("name").unwrap_or(""));
101
102 for node in root_nodes {
104 parse_node(
105 &robot_node,
106 node,
107 &robot_node,
108 WORLD_ID,
109 true,
110 &mut model,
111 &mut coll_model,
112 &mut viz_model,
113 &mut materials,
114 package_dir,
115 )?;
116 }
117
118 Ok((model, coll_model, viz_model))
119}
120
121fn parse_node(
144 robot_node: &Node,
145 node: Node,
146 parent_node: &Node,
147 parent_joint_id: usize,
148 is_root: bool,
149 model: &mut Model,
150 coll_model: &mut GeometryModel,
151 viz_model: &mut GeometryModel,
152 materials: &mut HashMap<String, Color>,
153 package_dir: Option<&str>,
154) -> Result<(), ParseError> {
155 let node_name = node.attribute("name").unwrap_or("");
156 let mut new_parent_joint_id = parent_joint_id;
157
158 let children = match node.tag_name().name() {
160 "link" => {
161 parse_link(
162 robot_node,
163 node,
164 parent_node,
165 parent_joint_id,
166 is_root,
167 model,
168 coll_model,
169 viz_model,
170 materials,
171 package_dir,
172 )?;
173
174 let mut children = Vec::new();
176
177 for joint_node in robot_node.children().filter(|n| n.has_tag_name("joint")) {
178 if let Some(parent_node) = joint_node.children().find(|n| n.has_tag_name("parent"))
183 && let Ok(parent_link_name) = extract_parameter::<String>("link", &parent_node)
184 && parent_link_name == node_name
185 {
186 children.push(joint_node);
188 }
189 }
190 children
191 }
192
193 "joint" => {
195 new_parent_joint_id =
196 parse_joint(robot_node, node, parent_node, parent_joint_id, model)?;
197
198 if let Some(child_node) = node.children().find(|n| n.has_tag_name("child")) {
200 let child_link_name = extract_parameter::<String>("link", &child_node)?;
202
203 if let Some(link_node) = robot_node.children().find(|n| {
205 n.has_tag_name("link") && n.attribute("name").unwrap_or("") == child_link_name
206 }) {
207 vec![link_node]
208 } else {
209 return Err(ParseError::UnknownLinkName(child_link_name));
211 }
212 } else {
213 vec![]
214 }
215 }
216
217 "" | "gazebo" | "transmission" => {
219 vec![]
220 }
221
222 _ => {
224 return Err(ParseError::UnknownTag(node.tag_name().name().to_string()));
225 }
226 };
227
228 let mut children: Vec<Node> = children.into_iter().collect();
230 children.sort_by_key(|n| n.attribute("name").unwrap_or(""));
231
232 for child_node in children {
234 parse_node(
235 robot_node,
236 child_node,
237 &node,
238 new_parent_joint_id,
239 false,
240 model,
241 coll_model,
242 viz_model,
243 materials,
244 package_dir,
245 )?;
246 }
247 Ok(())
248}
249
250fn find_root_nodes<'a>(robot_node: &'a Node) -> Result<HashSet<Node<'a, 'a>>, ParseError> {
260 let mut parent_links: HashSet<Node> = robot_node
262 .children()
263 .filter(|n| n.has_tag_name("link"))
264 .collect();
265
266 for joint_node in robot_node.children().filter(|n| n.has_tag_name("joint")) {
268 if let Some(child_node) = joint_node.children().find(|n| n.has_tag_name("child")) {
270 let child_link_name = extract_parameter::<String>("link", &child_node)?;
271 parent_links.retain(|link_node| {
273 let link_name = link_node.attribute("name").unwrap_or("");
274 link_name != child_link_name
275 });
276 }
277 }
278
279 Ok(parent_links)
280}
281
282fn parse_joint(
298 robot_node: &Node,
299 joint_node: Node,
300 parent_node: &Node,
301 parent_joint_id: usize,
302 model: &mut Model,
303) -> Result<usize, ParseError> {
304 let joint_name = joint_node
306 .attribute("name")
307 .ok_or(ParseError::NameMissing(format!("{joint_node:#?}")))?
308 .to_string();
309 let joint_type = joint_node
310 .attribute("type")
311 .ok_or(ParseError::MissingParameter("type".to_string()))?;
312
313 let parent_frame_id = get_parent_frame(parent_node, robot_node, model)?;
315 let parent_frame = &model.frames[parent_frame_id];
316
317 let link_origin = parse_origin(&joint_node)?;
319 let placement = parent_frame.placement * link_origin;
320
321 let new_joint_id = match joint_type {
322 "fixed" => {
324 let frame = Frame::new(
325 joint_name.clone(),
326 parent_joint_id,
327 parent_frame_id,
328 placement,
329 FrameType::Fixed,
330 Inertia::zeros(),
331 );
332 let _ = model.add_frame(frame, false);
333
334 Ok(parent_joint_id)
336 }
337
338 "continuous" => {
340 let axis = parse_axis(joint_node)?;
341 let mut joint_model = JointModelContinuous::new(axis);
342
343 if let Some(limit_node) = joint_node.children().find(|n| n.has_tag_name("limit")) {
345 let effort = extract_parameter::<f64>("effort", &limit_node)?;
346 joint_model.limits.effort[0] = effort;
347
348 let velocity = extract_parameter::<f64>("velocity", &limit_node)?;
349 joint_model.limits.velocity[0] = velocity;
350 }
351
352 model.add_joint(
353 parent_joint_id,
354 JointWrapper::continuous(joint_model),
355 placement,
356 joint_name.clone(),
357 )
358 }
359
360 "revolute" => {
362 let axis = parse_axis(joint_node)?;
363
364 let limit_node = joint_node
366 .children()
367 .find(|n| n.has_tag_name("limit"))
368 .ok_or(ParseError::MissingParameter("limit".to_string()))?;
369
370 let mut joint_model = JointModelRevolute::new(axis);
373
374 if let Ok(lower) = extract_parameter::<f64>("lower", &limit_node) {
376 joint_model.limits.min_configuration[0] = lower;
377 }
378 if let Ok(upper) = extract_parameter::<f64>("upper", &limit_node) {
379 joint_model.limits.max_configuration[0] = upper;
380 }
381
382 let effort = extract_parameter::<f64>("effort", &limit_node)?;
384 joint_model.limits.effort[0] = effort;
385
386 let velocity = extract_parameter::<f64>("velocity", &limit_node)?;
387 joint_model.limits.velocity[0] = velocity;
388
389 model.add_joint(
390 parent_joint_id,
391 JointWrapper::revolute(joint_model),
392 placement,
393 joint_name.clone(),
394 )
395 }
396
397 "prismatic" => {
398 let axis = parse_axis(joint_node)?;
399
400 let limit_node = joint_node
402 .children()
403 .find(|n| n.has_tag_name("limit"))
404 .ok_or(ParseError::MissingParameter("limit".to_string()))?;
405
406 let mut joint_model = JointModelPrismatic::new(axis);
409
410 if let Ok(lower) = extract_parameter::<f64>("lower", &limit_node) {
412 joint_model.limits.min_configuration[0] = lower;
413 }
414 if let Ok(upper) = extract_parameter::<f64>("upper", &limit_node) {
415 joint_model.limits.max_configuration[0] = upper;
416 }
417
418 let effort = extract_parameter::<f64>("effort", &limit_node)?;
420 joint_model.limits.effort[0] = effort;
421
422 let velocity = extract_parameter::<f64>("velocity", &limit_node)?;
423 joint_model.limits.velocity[0] = velocity;
424
425 model.add_joint(
426 parent_joint_id,
427 JointWrapper::prismatic(joint_model),
428 placement,
429 joint_name.clone(),
430 )
431 }
432
433 _ => return Err(ParseError::UnknownJointType(joint_type.to_string())),
434 }
435 .map_err(ParseError::ModelError)?;
436
437 if joint_type != "fixed" {
439 let frame = Frame::new(
440 joint_name,
441 new_joint_id,
442 parent_frame_id,
443 SE3::identity(),
444 FrameType::Joint,
445 Inertia::zeros(),
446 );
447 model
448 .add_frame(frame, true)
449 .map_err(ParseError::ModelError)?;
450 }
451
452 Ok(new_joint_id)
453}
454
455fn parse_axis(joint_node: Node) -> Result<Vector3D, ParseError> {
463 if let Some(axis_node) = joint_node.children().find(|n| n.has_tag_name("axis")) {
464 let axis_values = extract_parameter_list::<f64>("xyz", &axis_node, Some(3))?;
465 Ok(Vector3D::new(
466 axis_values[0],
467 axis_values[1],
468 axis_values[2],
469 ))
470 } else {
471 Ok(Vector3D::new(1.0, 0.0, 0.0))
473 }
474}
475
476fn parse_root_materials(robot_node: &Node) -> Result<HashMap<String, Color>, ParseError> {
484 let mut materials = HashMap::new();
485 for material_node in robot_node.children().filter(|n| n.has_tag_name("material")) {
486 parse_material(material_node, &mut materials)?;
487 }
488 Ok(materials)
489}
490
491fn parse_link(
512 robot_node: &Node,
513 node: Node,
514 parent_node: &Node,
515 parent_joint_id: usize,
516 is_root: bool,
517 model: &mut Model,
518 coll_model: &mut GeometryModel,
519 viz_model: &mut GeometryModel,
520 materials: &mut HashMap<String, Color>,
521 package_dir: Option<&str>,
522) -> Result<(), ParseError> {
523 let link_name = node.attribute("name").unwrap_or("").to_string();
524 let parent_frame_id = get_parent_frame(parent_node, robot_node, model)?;
525
526 let link_inertia = parse_inertia(node, &link_name)?;
528
529 let frame_inertia = if is_root {
533 link_inertia
534 }
535 else if let Some(parent_joint_type) = parent_node.attribute("type")
537 && parent_joint_type == "fixed"
538 {
539 model.frames[parent_frame_id].inertia += link_inertia.clone(); model.inertias[parent_joint_id] +=
541 link_inertia.act(&model.frames[parent_frame_id].placement);
542 Inertia::zeros()
543 } else {
544 model.inertias[parent_joint_id] +=
545 link_inertia.act(&model.frames[parent_frame_id].placement);
546 Inertia::zeros()
547 };
548
549 let parent_frame = &model.frames[parent_frame_id];
551 let link_placement = parent_frame.placement * parse_origin(&node)?;
552
553 let frame = Frame::new(
555 link_name.clone(),
556 parent_joint_id,
557 parent_frame_id,
558 link_placement,
559 FrameType::Body,
560 frame_inertia,
561 );
562 let new_frame_id = model
563 .add_frame(frame, true)
564 .map_err(ParseError::ModelError)?;
565
566 for (i, collision_node) in node
568 .children()
569 .filter(|n| n.has_tag_name("collision"))
570 .enumerate()
571 {
572 let geom_obj = parse_geometry(
573 format!("{link_name}_{i}"),
574 &collision_node,
575 parent_joint_id,
576 new_frame_id,
577 model,
578 materials,
579 package_dir,
580 GeometryType::Collision,
581 )?;
582 coll_model.add_geometry_object(geom_obj);
583 }
584
585 for (i, visual_node) in node
587 .children()
588 .filter(|n| n.has_tag_name("visual"))
589 .enumerate()
590 {
591 let geom_obj = parse_geometry(
592 format!("{link_name}_{i}"),
593 &visual_node,
594 parent_joint_id,
595 new_frame_id,
596 model,
597 materials,
598 package_dir,
599 GeometryType::Visual,
600 )?;
601 viz_model.add_geometry_object(geom_obj);
602 }
603
604 Ok(())
605}
606
607fn get_parent_frame(
609 parent_node: &Node,
610 robot_node: &Node,
611 model: &Model,
612) -> Result<usize, ParseError> {
613 let parent_name = parent_node
615 .attribute("name")
616 .ok_or(ParseError::NameMissing(format!("{parent_node:#?}")))?;
617 let robot_name = robot_node
618 .attribute("name")
619 .ok_or(ParseError::NameMissing(format!("{robot_node:#?}")))?;
620
621 if parent_name == robot_name {
623 Ok(WORLD_ID)
624 }
625 else {
627 let parent_type = parent_node.attribute("type").unwrap_or("");
629 let frame_type = match parent_type {
630 "fixed" => FrameType::Fixed,
631 "revolute" | "continuous" | "prismatic" => FrameType::Joint,
632 _ => FrameType::Body,
633 };
634 model
635 .get_frame_id(parent_name, Some(&frame_type))
636 .ok_or(ParseError::UnknownParent(parent_name.to_string()))
637 }
638}
639
640fn parse_material(node: Node, materials: &mut HashMap<String, Color>) -> Result<(), ParseError> {
650 let material_name = node
652 .attribute("name")
653 .ok_or(ParseError::NameMissing(format!("{node:#?}")))?
654 .to_string();
655
656 let color_node = node
658 .children()
659 .find(|n| n.has_tag_name("color"))
660 .ok_or(ParseError::MaterialWithoutColor(material_name.clone()))?;
661 let rgba = extract_parameter_list::<f64>("rgba", &color_node, Some(4))?;
662 let color = Color::new(rgba[0], rgba[1], rgba[2], rgba[3]);
663
664 materials.insert(material_name, color);
667 Ok(())
668}
669
670fn parse_inertia(node: Node, link_name: &str) -> Result<Inertia, ParseError> {
680 if let Some(inertial_node) = node.children().find(|n| n.has_tag_name("inertial")) {
681 let mass_node = inertial_node
682 .children()
683 .find(|n| n.has_tag_name("mass"))
684 .ok_or(ParseError::InertialWithoutMass(link_name.to_string()))?;
685 let mass = extract_parameter::<f64>("value", &mass_node)?;
686
687 let inertia_node = inertial_node
688 .children()
689 .find(|n| n.has_tag_name("inertia"))
690 .ok_or(ParseError::InertialWithoutInertia(link_name.to_string()))?;
691
692 let ixx = extract_parameter::<f64>("ixx", &inertia_node)?;
693 let ixy = extract_parameter::<f64>("ixy", &inertia_node)?;
694 let ixz = extract_parameter::<f64>("ixz", &inertia_node)?;
695 let iyy = extract_parameter::<f64>("iyy", &inertia_node)?;
696 let iyz = extract_parameter::<f64>("iyz", &inertia_node)?;
697 let izz = extract_parameter::<f64>("izz", &inertia_node)?;
698 let inertia_mat = Symmetric3::new(ixx, iyy, izz, ixy, ixz, iyz);
699
700 let inertial_origin = if let Some(origin_node) =
701 inertial_node.children().find(|n| n.has_tag_name("origin"))
702 {
703 let rpy =
705 extract_parameter_list::<f64>("rpy", &origin_node, Some(3)).unwrap_or(vec![0.0; 3]);
706 let xyz =
707 extract_parameter_list::<f64>("xyz", &origin_node, Some(3)).unwrap_or(vec![0.0; 3]);
708
709 let rotation = SpatialRotation::from_euler_angles(rpy[0], rpy[1], rpy[2]);
711 let translation = Vector3D::new(xyz[0], xyz[1], xyz[2]);
712
713 SE3::from_parts(translation, rotation)
714 } else {
715 SE3::identity()
717 };
718
719 Ok(Inertia::new(
720 mass,
721 inertial_origin.translation(),
722 inertia_mat.rotate(&inertial_origin.rotation()),
723 ))
724 } else {
725 Ok(Inertia::zeros())
726 }
727}
728
729fn parse_geometry(
746 link_name: String,
747 node: &Node,
748 parent_joint_id: usize,
749 parent_frame_id: usize,
750 model: &Model,
751 materials: &mut HashMap<String, Color>,
752 package_dir: Option<&str>,
753 geometry_type: GeometryType,
754) -> Result<GeometryObject, ParseError> {
755 let geometry_node = node
756 .children()
757 .find(|n| n.has_tag_name("geometry"))
758 .ok_or(ParseError::VisualWithoutGeometry(link_name.clone()))?;
759
760 let shape: ShapeWrapper = if let Some(shape_node) =
762 geometry_node.children().find(|n| n.has_tag_name("box"))
763 {
764 let size = extract_parameter_list::<f32>("size", &shape_node, Some(3))?;
765 let half_extents = Vector3::new(size[0] / 2.0, size[1] / 2.0, size[2] / 2.0);
766 Box::new(Cuboid::new(half_extents))
767 } else if let Some(shape_node) = geometry_node
768 .children()
769 .find(|n| n.has_tag_name("cylinder"))
770 {
771 let radius = extract_parameter::<f32>("radius", &shape_node)?;
772 let length = extract_parameter::<f32>("length", &shape_node)?;
773 Box::new(Cylinder::new(radius, length / 2.0))
774 } else if let Some(shape_node) = geometry_node.children().find(|n| n.has_tag_name("sphere")) {
775 let radius = extract_parameter::<f32>("radius", &shape_node)?;
776 Box::new(Sphere::new(radius))
777 } else if let Some(mesh_node) = geometry_node.children().find(|n| n.has_tag_name("mesh")) {
778 let filename = mesh_node
779 .attribute("filename")
780 .ok_or(ParseError::MissingParameter("filename".to_string()))?;
781
782 let absolute_path = if let Some(filename) = filename.strip_prefix("package://") {
783 let filename = filename.strip_prefix('/').unwrap_or(filename);
785
786 let path_parts: Vec<&str> = filename.splitn(2, '/').collect();
788 if path_parts.len() != 2 {
789 return Err(ParseError::InvalidFilePath(format!(
790 "Invalid package path: {filename}"
791 )));
792 }
793 let package_name = path_parts[0];
794 let relative_path = path_parts[1];
795
796 let ros_package_path = std::env::var("ROS_PACKAGE_PATH").map_err(|_| ParseError::InvalidFilePath(
798 "'package://' was used but the ROS_PACKAGE_PATH environment variable is not set".to_string(),
799 ))?;
800 let package_paths: Vec<&str> = ros_package_path
801 .split(':')
802 .filter(|s| !s.is_empty())
803 .collect();
804
805 let mut package_path = None;
807 for path in package_paths {
808 let path_obj = Path::new(path);
810
811 if let Some(folder_name) = path_obj.file_name().and_then(|s| s.to_str())
813 && folder_name == package_name
814 {
815 package_path = Some(path_obj.to_path_buf());
816 break;
817 }
818 }
819
820 let package_path = package_path.ok_or(ParseError::InvalidFilePath(format!(
821 "Package '{package_name}' not found in ROS_PACKAGE_PATH",
822 )))?;
823
824 package_path
826 .join(relative_path)
827 .to_str()
828 .ok_or(ParseError::InvalidFilePath(format!(
829 "Invalide path: {}",
830 package_path.join(relative_path).display()
831 )))?
832 .to_string()
833 } else if filename.starts_with('/') {
834 filename.to_string()
835 } else {
836 let package_dir = package_dir.ok_or(ParseError::InvalidFilePath(
838 "'package_dir' must be provided to resolve mesh file paths".to_string(),
839 ))?;
840 let package_dir = Path::new(package_dir);
841 package_dir
842 .join(filename)
843 .to_str()
844 .ok_or(ParseError::InvalidFilePath(format!(
845 "Invalide path: {}",
846 package_dir.join(filename).display()
847 )))?
848 .to_string()
849 };
850
851 if !std::path::Path::new(&absolute_path).exists() {
853 return Err(ParseError::InvalidFilePath(format!(
854 "Mesh file does not exist: {absolute_path}"
855 )));
856 }
857
858 Box::new(Mesh::new(absolute_path))
859 } else {
860 return Err(ParseError::GeometryWithoutShape(link_name.clone()));
861 };
862
863 let parent_frame = &model.frames[parent_frame_id];
865 let placement = parent_frame.placement * parse_origin(node)?;
866
867 let mut color = Color::new(0.9, 0.9, 0.9, 1.0);
869 if geometry_type != GeometryType::Visual {
870 }
872 else if let Some(material_node) = node.children().find(|n| n.has_tag_name("material")) {
874 if let Some(material_name) = material_node.attribute("name")
877 && let Some(material_color) = materials.get(material_name)
878 {
879 color = *material_color;
880 }
881 else if let Some(color_node) = material_node.children().find(|n| n.has_tag_name("color"))
883 && let Ok(rgba) = extract_parameter_list::<f64>("rgba", &color_node, Some(4))
884 {
885 color = Color::new(rgba[0], rgba[1], rgba[2], rgba[3]);
886
887 if let Some(material_name) = material_node.attribute("name")
889 && !material_name.is_empty()
890 {
891 materials.insert(material_name.to_string(), color);
892 }
893 }
894 }
895
896 let geom_obj = GeometryObject::new(
897 link_name,
898 parent_joint_id,
899 parent_frame_id,
900 shape,
901 color,
902 placement,
903 );
904 Ok(geom_obj)
905}
906
907fn extract_parameter<T: FromStr>(name: &str, node: &roxmltree::Node) -> Result<T, ParseError> {
910 node.attribute(name)
911 .ok_or_else(|| ParseError::MissingParameter(name.to_string()))?
912 .parse::<T>()
913 .map_err(|_| ParseError::InvalidParameter(name.to_string()))
914}
915
916fn extract_parameter_list<T: FromStr>(
919 name: &str,
920 node: &roxmltree::Node,
921 expected_length: Option<usize>,
922) -> Result<Vec<T>, ParseError> {
923 let vector = node
924 .attribute(name)
925 .ok_or_else(|| ParseError::MissingParameter(name.to_string()))?
926 .split_whitespace()
927 .map(|s| {
928 s.parse::<T>()
929 .map_err(|_| ParseError::InvalidParameter(name.to_string()))
930 })
931 .collect::<Result<Vec<T>, ParseError>>()?;
932 if let Some(expected_length) = expected_length
933 && vector.len() != expected_length
934 {
935 return Err(ParseError::InvalidParameter(name.to_string()));
936 }
937 Ok(vector)
938}
939
940fn parse_origin(node: &roxmltree::Node) -> Result<SE3, ParseError> {
941 let isometry = if let Some(origin_node) = node.children().find(|n| n.has_tag_name("origin")) {
942 let xyz = extract_parameter_list::<f64>("xyz", &origin_node, Some(3))?;
943 let rotation = match extract_parameter_list::<f64>("rpy", &origin_node, Some(3)) {
944 Ok(rpy) => SpatialRotation::from_euler_angles(rpy[0], rpy[1], rpy[2]),
945 Err(ParseError::MissingParameter(_)) => SpatialRotation::identity(),
946 Err(e) => return Err(e),
947 };
948 let translation = Vector3D::new(xyz[0], xyz[1], xyz[2]);
949
950 SE3::from_parts(translation, rotation)
951 } else {
952 SE3::identity()
953 };
954 Ok(isometry)
955}