from __future__ import annotations import numpy as np import os from .message import warning from .robot import Robot, Link, Part, Joint from .config import Config from .geometry import Box, Cylinder, Sphere, Shape, Mesh from .exporter import Exporter from .exporter_utils import xml_escape, rotation_matrix_to_rpy class ExporterURDF(Exporter): def __init__(self, config: Config | None = None): super().__init__() self.config: Config = config self.ext: str = "urdf" self.no_dynamics: bool = False self.package_name: str = "" self.additional_xml: str = "" self.set_zero_mass_to_fixed: bool = False if config is not None: self.no_dynamics = config.no_dynamics self.package_name: str = config.get("package_name", "") self.set_zero_mass_to_fixed: bool = config.get( "set_zero_mass_to_fixed", False ) additional_xml_file = config.get("additional_xml", None, required=False) if isinstance(additional_xml_file, str): self.add_additional_xml(additional_xml_file) elif isinstance(additional_xml_file, list): for filename in additional_xml_file: self.add_additional_xml(filename) def add_additional_xml(self, xml_file: str): self.additional_xml += f"" with open(self.config.output_directory + "/" + xml_file, "r") as file: self.additional_xml += file.read() def append(self, line: str): self.xml += line def build(self, robot: Robot): self.xml = "" self.append('') self.append("") if self.config: self.append(f"") self.append(f'') if len(robot.base_links) > 1: print( warning( "WARNING: Multiple base links detected, which is not supported by URDF." ) ) print(warning("Only the first base link will be considered.")) if len(robot.base_links) > 0: self.add_link(robot, robot.base_links[0]) if self.additional_xml: self.append(self.additional_xml) self.append("") return self.xml def add_inertial( self, mass: float, com: np.ndarray, inertia: np.ndarray, fixed: str = False ): # Unless "no_dynamics" is set, we make sure that mass and inertia # are not zero if not self.no_dynamics: mass = max(1e-9, mass) inertia[0, 0] = max(1e-9, inertia[0, 0]) inertia[1, 1] = max(1e-9, inertia[1, 1]) inertia[2, 2] = max(1e-9, inertia[2, 2]) if fixed and self.set_zero_mass_to_fixed: # To mark an object as fixed in the world, sets its dynamics to zero mass = 0 com = np.zeros(3) inertia = np.zeros((3, 3)) self.append("") self.append( '' % self.config.round( ( com[0], com[1], com[2], ) ) ) self.append('' % self.config.round(mass)) self.append( '' % self.config.round( ( inertia[0, 0], inertia[0, 1], inertia[0, 2], inertia[1, 1], inertia[1, 2], inertia[2, 2], ) ) ) self.append("") def add_mesh(self, part: Part, node: str, T_world_link: np.ndarray, mesh: Mesh): """ Add a mesh node (e.g. STL) to the URDF file """ self.append(f"<{node}>") T_link_part = np.linalg.inv(T_world_link) @ part.T_world_part self.append(self.origin(T_link_part)) mesh_file = mesh.filename if self.package_name: mesh_file = self.package_name + "/" + mesh_file self.append("") self.append(f'') self.append("") if node == "visual": material_name = f"{part.name}_material" self.append(f'') self.append( '' % self.config.round( (mesh.color[0], mesh.color[1], mesh.color[2], mesh.color[3]) ) ) self.append("") # Apply properties based on node type (visual or collision) properties = mesh.visual_properties if node == "visual" else mesh.collision_properties for key, value in properties.items(): self.append(f'<{key}>{xml_escape(str(value))}') self.append(f"") def add_shape(self, part: Part, node: str, T_world_link: np.ndarray, shape: Shape): """ Add shapes (box, sphere and cylinder) nodes to the URDF. """ self.append(f"<{node}>") T_link_shape = ( np.linalg.inv(T_world_link) @ part.T_world_part @ shape.T_part_shape ) self.append(self.origin(T_link_shape)) self.append("") if isinstance(shape, Box): self.append( '' % self.config.round(tuple(shape.size)) ) elif isinstance(shape, Cylinder): self.append( '' % self.config.round((shape.length, shape.radius)) ) elif isinstance(shape, Sphere): self.append('' % self.config.round((shape.radius,))) self.append("") if node == "visual": material_name = f"{part.name}_material" self.append(f'') self.append( '' % self.config.round( (shape.color[0], shape.color[1], shape.color[2], shape.color[3]) ) ) self.append("") # Apply properties based on node type (visual or collision) properties = shape.visual_properties if node == "visual" else shape.collision_properties for key, value in properties.items(): self.append(f'<{key}>{xml_escape(str(value))}') self.append(f"") def add_geometries(self, part: Part, T_world_link: np.ndarray): """ Add a part geometries """ for shape in part.shapes: if shape.visual: self.add_shape(part, "visual", T_world_link, shape) if shape.collision: self.add_shape(part, "collision", T_world_link, shape) for mesh in part.meshes: if mesh.visual: self.add_mesh(part, "visual", T_world_link, mesh) if mesh.collision: self.add_mesh(part, "collision", T_world_link, mesh) def add_joint(self, joint: Joint, T_world_link: np.ndarray): self.append(f"") joint_type = joint.properties.get("type", joint.joint_type) self.append(f'') T_link_joint = np.linalg.inv(T_world_link) @ joint.T_world_joint self.append(self.origin(T_link_joint)) self.append(f'') self.append(f'') self.append('' % self.config.round(tuple(joint.axis))) limits = "" if "max_effort" in joint.properties: limits += 'effort="%g" ' % self.config.round(joint.properties["max_effort"]) else: limits += 'effort="10" ' if "max_velocity" in joint.properties: limits += 'velocity="%g" ' % self.config.round( joint.properties["max_velocity"] ) else: limits += 'velocity="10" ' joint_limits = joint.properties.get("limits", joint.limits) if joint_limits is not None: limits += 'lower="%g" upper="%g" ' % self.config.round( (joint_limits[0], joint_limits[1]) ) elif joint_type == "revolute": limits += f'lower="{-np.pi}" upper="{np.pi}" ' elif joint_type == "prismatic": limits += 'lower="-1" upper="1" ' if limits: self.append(f"") if "friction" in joint.properties: self.append( f'' ) if joint.relation is not None: self.append( f'' ) self.append("") def add_frame( self, link: Link, frame: str, T_world_link: np.ndarray, T_world_frame: np.ndarray, ): self.append(f"") T_link_frame = np.linalg.inv(T_world_link) @ T_world_frame # Adding a dummy link to the assembly self.append(f'') self.append(self.origin(np.eye(4))) self.append("") self.append('') if self.no_dynamics: self.append('') else: self.append('') self.append('') self.append("") self.append("") # Attaching this dummy link to the parent frame using a fixed joint self.append(f'') self.append(self.origin(T_link_frame)) self.append(f'') self.append(f'') self.append('') self.append("") def add_link(self, robot: Robot, link: Link, T_world_link: np.ndarray = np.eye(4)): """ Adds a link recursively to the URDF file """ self.append(f"") self.append(f'') # Adding inertial properties mass, com, inertia = link.get_dynamics(T_world_link) self.add_inertial(mass, com, inertia, link.fixed) # Adding geometry objects for part in link.parts: self.append(f"") self.add_geometries(part, T_world_link) self.append("") # Adding frames attached to current link for frame, T_world_frame in link.frames.items(): self.add_frame(link, frame, T_world_link, T_world_frame) # Adding joints and children links for joint in robot.get_link_joints(link): self.add_link(robot, joint.child, joint.T_world_joint) self.add_joint(joint, T_world_link) def origin(self, matrix: np.ndarray): """ Transforms a transformation matrix into a URDF origin tag """ urdf = '' return urdf % self.config.round( (*matrix[:3, 3], *rotation_matrix_to_rpy(matrix)) )