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::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 {
537 if let Some(parent_joint_type) = parent_node.attribute("type")
538 && parent_joint_type == "fixed"
539 {
540 model.frames[parent_frame_id].inertia += &link_inertia;
541 }
542 model
543 .append_body_to_joint(
544 parent_joint_id,
545 &link_inertia,
546 model.frames[parent_frame_id].placement,
547 )
548 .map_err(ParseError::ModelError)?;
549 Inertia::zeros()
550 };
551
552 let parent_frame = &model.frames[parent_frame_id];
554 let link_placement = parent_frame.placement * parse_origin(&node)?;
555
556 let frame = Frame::new(
558 link_name.clone(),
559 parent_joint_id,
560 parent_frame_id,
561 link_placement,
562 FrameType::Body,
563 frame_inertia,
564 );
565 let new_frame_id = model
566 .add_frame(frame, true)
567 .map_err(ParseError::ModelError)?;
568
569 for (i, collision_node) in node
571 .children()
572 .filter(|n| n.has_tag_name("collision"))
573 .enumerate()
574 {
575 let geom_obj = parse_geometry(
576 format!("{link_name}_{i}"),
577 &collision_node,
578 parent_joint_id,
579 new_frame_id,
580 model,
581 materials,
582 package_dir,
583 GeometryType::Collision,
584 )?;
585 coll_model.add_geometry_object(geom_obj);
586 }
587
588 for (i, visual_node) in node
590 .children()
591 .filter(|n| n.has_tag_name("visual"))
592 .enumerate()
593 {
594 let geom_obj = parse_geometry(
595 format!("{link_name}_{i}"),
596 &visual_node,
597 parent_joint_id,
598 new_frame_id,
599 model,
600 materials,
601 package_dir,
602 GeometryType::Visual,
603 )?;
604 viz_model.add_geometry_object(geom_obj);
605 }
606
607 Ok(())
608}
609
610fn get_parent_frame(
612 parent_node: &Node,
613 robot_node: &Node,
614 model: &Model,
615) -> Result<usize, ParseError> {
616 let parent_name = parent_node
618 .attribute("name")
619 .ok_or(ParseError::NameMissing(format!("{parent_node:#?}")))?;
620 let robot_name = robot_node
621 .attribute("name")
622 .ok_or(ParseError::NameMissing(format!("{robot_node:#?}")))?;
623
624 if parent_name == robot_name {
626 Ok(WORLD_ID)
627 }
628 else {
630 let parent_type = parent_node.attribute("type").unwrap_or("");
632 let frame_type = match parent_type {
633 "fixed" => FrameType::Fixed,
634 "revolute" | "continuous" | "prismatic" => FrameType::Joint,
635 _ => FrameType::Body,
636 };
637 model
638 .get_frame_id(parent_name, Some(&frame_type))
639 .ok_or(ParseError::UnknownParent(parent_name.to_string()))
640 }
641}
642
643fn parse_material(node: Node, materials: &mut HashMap<String, Color>) -> Result<(), ParseError> {
653 let material_name = node
655 .attribute("name")
656 .ok_or(ParseError::NameMissing(format!("{node:#?}")))?
657 .to_string();
658
659 let color_node = node
661 .children()
662 .find(|n| n.has_tag_name("color"))
663 .ok_or(ParseError::MaterialWithoutColor(material_name.clone()))?;
664 let rgba = extract_parameter_list::<f64>("rgba", &color_node, Some(4))?;
665 let color = Color::new(rgba[0], rgba[1], rgba[2], rgba[3]);
666
667 materials.insert(material_name, color);
670 Ok(())
671}
672
673fn parse_inertia(node: Node, link_name: &str) -> Result<Inertia, ParseError> {
683 if let Some(inertial_node) = node.children().find(|n| n.has_tag_name("inertial")) {
684 let mass_node = inertial_node
685 .children()
686 .find(|n| n.has_tag_name("mass"))
687 .ok_or(ParseError::InertialWithoutMass(link_name.to_string()))?;
688 let mass = extract_parameter::<f64>("value", &mass_node)?;
689
690 let inertia_node = inertial_node
691 .children()
692 .find(|n| n.has_tag_name("inertia"))
693 .ok_or(ParseError::InertialWithoutInertia(link_name.to_string()))?;
694
695 let ixx = extract_parameter::<f64>("ixx", &inertia_node)?;
696 let ixy = extract_parameter::<f64>("ixy", &inertia_node)?;
697 let ixz = extract_parameter::<f64>("ixz", &inertia_node)?;
698 let iyy = extract_parameter::<f64>("iyy", &inertia_node)?;
699 let iyz = extract_parameter::<f64>("iyz", &inertia_node)?;
700 let izz = extract_parameter::<f64>("izz", &inertia_node)?;
701 let inertia_mat = Symmetric3::new(ixx, iyy, izz, ixy, ixz, iyz);
702
703 let inertial_origin = if let Some(origin_node) =
704 inertial_node.children().find(|n| n.has_tag_name("origin"))
705 {
706 let rpy =
708 extract_parameter_list::<f64>("rpy", &origin_node, Some(3)).unwrap_or(vec![0.0; 3]);
709 let xyz =
710 extract_parameter_list::<f64>("xyz", &origin_node, Some(3)).unwrap_or(vec![0.0; 3]);
711
712 let rotation = SpatialRotation::from_euler_angles(rpy[0], rpy[1], rpy[2]);
714 let translation = Vector3D::new(xyz[0], xyz[1], xyz[2]);
715
716 SE3::from_parts(translation, rotation)
717 } else {
718 SE3::identity()
720 };
721
722 Ok(Inertia::new(
723 mass,
724 inertial_origin.translation(),
725 inertia_mat.rotate(&inertial_origin.rotation()),
726 ))
727 } else {
728 Ok(Inertia::zeros())
729 }
730}
731
732fn parse_geometry(
749 link_name: String,
750 node: &Node,
751 parent_joint_id: usize,
752 parent_frame_id: usize,
753 model: &Model,
754 materials: &mut HashMap<String, Color>,
755 package_dir: Option<&str>,
756 geometry_type: GeometryType,
757) -> Result<GeometryObject, ParseError> {
758 let geometry_node = node
759 .children()
760 .find(|n| n.has_tag_name("geometry"))
761 .ok_or(ParseError::VisualWithoutGeometry(link_name.clone()))?;
762
763 let shape: ShapeWrapper = if let Some(shape_node) =
765 geometry_node.children().find(|n| n.has_tag_name("box"))
766 {
767 let size = extract_parameter_list::<f32>("size", &shape_node, Some(3))?;
768 let half_extents = Vector3::new(size[0] / 2.0, size[1] / 2.0, size[2] / 2.0);
769 Box::new(Cuboid::new(half_extents))
770 } else if let Some(shape_node) = geometry_node
771 .children()
772 .find(|n| n.has_tag_name("cylinder"))
773 {
774 let radius = extract_parameter::<f32>("radius", &shape_node)?;
775 let length = extract_parameter::<f32>("length", &shape_node)?;
776 Box::new(Cylinder::new(radius, length / 2.0))
777 } else if let Some(shape_node) = geometry_node.children().find(|n| n.has_tag_name("sphere")) {
778 let radius = extract_parameter::<f32>("radius", &shape_node)?;
779 Box::new(Sphere::new(radius))
780 } else if let Some(mesh_node) = geometry_node.children().find(|n| n.has_tag_name("mesh")) {
781 let filename = mesh_node
782 .attribute("filename")
783 .ok_or(ParseError::MissingParameter("filename".to_string()))?;
784
785 let absolute_path = if let Some(filename) = filename.strip_prefix("package://") {
786 let filename = filename.strip_prefix('/').unwrap_or(filename);
788
789 let path_parts: Vec<&str> = filename.splitn(2, '/').collect();
791 if path_parts.len() != 2 {
792 return Err(ParseError::InvalidFilePath(format!(
793 "Invalid package path: {filename}"
794 )));
795 }
796 let package_name = path_parts[0];
797 let relative_path = path_parts[1];
798
799 let ros_package_path = std::env::var("ROS_PACKAGE_PATH").map_err(|_| ParseError::InvalidFilePath(
801 "'package://' was used but the ROS_PACKAGE_PATH environment variable is not set".to_string(),
802 ))?;
803 let package_paths: Vec<&str> = ros_package_path
804 .split(':')
805 .filter(|s| !s.is_empty())
806 .collect();
807
808 let mut package_path = None;
810 for path in package_paths {
811 let path_obj = Path::new(path);
813
814 if let Some(folder_name) = path_obj.file_name().and_then(|s| s.to_str())
816 && folder_name == package_name
817 {
818 package_path = Some(path_obj.to_path_buf());
819 break;
820 }
821 }
822
823 let package_path = package_path.ok_or(ParseError::InvalidFilePath(format!(
824 "Package '{package_name}' not found in ROS_PACKAGE_PATH",
825 )))?;
826
827 package_path
829 .join(relative_path)
830 .to_str()
831 .ok_or(ParseError::InvalidFilePath(format!(
832 "Invalide path: {}",
833 package_path.join(relative_path).display()
834 )))?
835 .to_string()
836 } else if filename.starts_with('/') {
837 filename.to_string()
838 } else {
839 let package_dir = package_dir.ok_or(ParseError::InvalidFilePath(
841 "'package_dir' must be provided to resolve mesh file paths".to_string(),
842 ))?;
843 let package_dir = Path::new(package_dir);
844 package_dir
845 .join(filename)
846 .to_str()
847 .ok_or(ParseError::InvalidFilePath(format!(
848 "Invalide path: {}",
849 package_dir.join(filename).display()
850 )))?
851 .to_string()
852 };
853
854 if !std::path::Path::new(&absolute_path).exists() {
856 return Err(ParseError::InvalidFilePath(format!(
857 "Mesh file does not exist: {absolute_path}"
858 )));
859 }
860
861 Box::new(Mesh::new(absolute_path))
862 } else {
863 return Err(ParseError::GeometryWithoutShape(link_name.clone()));
864 };
865
866 let parent_frame = &model.frames[parent_frame_id];
868 let placement = parent_frame.placement * parse_origin(node)?;
869
870 let mut color = Color::new(0.9, 0.9, 0.9, 1.0);
872 if geometry_type != GeometryType::Visual {
873 }
875 else if let Some(material_node) = node.children().find(|n| n.has_tag_name("material")) {
877 if let Some(material_name) = material_node.attribute("name")
880 && let Some(material_color) = materials.get(material_name)
881 {
882 color = *material_color;
883 }
884 else if let Some(color_node) = material_node.children().find(|n| n.has_tag_name("color"))
886 && let Ok(rgba) = extract_parameter_list::<f64>("rgba", &color_node, Some(4))
887 {
888 color = Color::new(rgba[0], rgba[1], rgba[2], rgba[3]);
889
890 if let Some(material_name) = material_node.attribute("name")
892 && !material_name.is_empty()
893 {
894 materials.insert(material_name.to_string(), color);
895 }
896 }
897 }
898
899 let geom_obj = GeometryObject::new(
900 link_name,
901 parent_joint_id,
902 parent_frame_id,
903 shape,
904 color,
905 placement,
906 );
907 Ok(geom_obj)
908}
909
910fn extract_parameter<T: FromStr>(name: &str, node: &roxmltree::Node) -> Result<T, ParseError> {
913 node.attribute(name)
914 .ok_or_else(|| ParseError::MissingParameter(name.to_string()))?
915 .parse::<T>()
916 .map_err(|_| ParseError::InvalidParameter(name.to_string()))
917}
918
919fn extract_parameter_list<T: FromStr>(
922 name: &str,
923 node: &roxmltree::Node,
924 expected_length: Option<usize>,
925) -> Result<Vec<T>, ParseError> {
926 let vector = node
927 .attribute(name)
928 .ok_or_else(|| ParseError::MissingParameter(name.to_string()))?
929 .split_whitespace()
930 .map(|s| {
931 s.parse::<T>()
932 .map_err(|_| ParseError::InvalidParameter(name.to_string()))
933 })
934 .collect::<Result<Vec<T>, ParseError>>()?;
935 if let Some(expected_length) = expected_length
936 && vector.len() != expected_length
937 {
938 return Err(ParseError::InvalidParameter(name.to_string()));
939 }
940 Ok(vector)
941}
942
943fn parse_origin(node: &roxmltree::Node) -> Result<SE3, ParseError> {
944 let isometry = if let Some(origin_node) = node.children().find(|n| n.has_tag_name("origin")) {
945 let xyz = extract_parameter_list::<f64>("xyz", &origin_node, Some(3))?;
946 let rotation = match extract_parameter_list::<f64>("rpy", &origin_node, Some(3)) {
947 Ok(rpy) => SpatialRotation::from_euler_angles(rpy[0], rpy[1], rpy[2]),
948 Err(ParseError::MissingParameter(_)) => SpatialRotation::identity(),
949 Err(e) => return Err(e),
950 };
951 let translation = Vector3D::new(xyz[0], xyz[1], xyz[2]);
952
953 SE3::from_parts(translation, rotation)
954 } else {
955 SE3::identity()
956 };
957 Ok(isometry)
958}