from __future__ import annotations
import numpy as np
import os
from .message import success
from .robot import Robot, Link, Part, Joint
from .config import Config
from .geometry import Box, Cylinder, Sphere, Mesh, Shape
from .exporter import Exporter
from .exporter_utils import xml_escape, rotation_matrix_to_rpy
MODEL_CONFIG_XML = """
%s
1.0
%s
"""
class ExporterSDF(Exporter):
def __init__(self, config: Config | None = None):
super().__init__()
self.config: Config = config
self.ext: str = "sdf"
self.no_dynamics: bool = False
self.additional_xml: str = ""
if config is not None:
self.no_dynamics = config.no_dynamics
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('')
self.append(f'')
for base_link in robot.base_links:
self.add_link(robot, base_link)
if self.additional_xml:
self.append(self.additional_xml)
self.append("")
self.append("")
return self.xml
def add_inertial(
self, mass: float, com: np.ndarray, inertia: np.ndarray, frame: str = ""
):
# 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])
self.append("")
self.append(
'%g %g %g 0 0 0'
% self.config.round((
com[0],
com[1],
com[2],
))
)
self.append("%g" % self.config.round(mass))
self.append(
"%g%g%g%g%g%g"
% self.config.round((
inertia[0, 0],
inertia[0, 1],
inertia[0, 2],
inertia[1, 1],
inertia[1, 2],
inertia[2, 2],
))
)
self.append("")
def append_material(self, color: np.ndarray):
self.append(f"")
self.append("%g %g %g %g" % self.config.round((color[0], color[1], color[2], color[3])))
self.append("%g %g %g %g" % self.config.round((color[0], color[1], color[2], color[3])))
self.append("0.1 0.1 0.1 1")
self.append("0 0 0 0")
self.append("")
def add_mesh(
self,
link: Link,
part: Part,
node: str,
T_world_link: np.ndarray,
mesh: Mesh,
mesh_n: int,
):
"""
Add a mesh node (e.g. STL) to the SDF file
"""
self.append(f'<{node} name="{part.name}_{node}_mesh_{mesh_n}">')
T_link_part = np.linalg.inv(T_world_link) @ part.T_world_part
self.append(self.pose(T_link_part, relative_to=link.name))
mesh_file = mesh.filename
self.append("")
self.append(
f"model://{self.config.robot_name}/{xml_escape(mesh_file)}"
)
self.append("")
if node == "visual":
self.append_material(mesh.color)
# 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))}{key}>')
self.append(f"{node}>")
def add_shape(
self,
link: Link,
part: Part,
node: str,
T_world_link: np.ndarray,
shape: Shape,
shape_n: int,
):
"""
Add shapes (box, sphere and cylinder) nodes to the SDF.
"""
self.append(f'<{node} name="{part.name}_{node}_shapes_{shape_n}">')
T_link_shape = (
np.linalg.inv(T_world_link) @ part.T_world_part @ shape.T_part_shape
)
self.append(self.pose(T_link_shape, relative_to=link.name))
self.append("")
if isinstance(shape, Box):
self.append("%g %g %g" % self.config.round(tuple(shape.size)))
elif isinstance(shape, Cylinder):
self.append(
"%g%g"
% self.config.round((shape.length, shape.radius))
)
elif isinstance(shape, Sphere):
self.append("%g" % self.config.round((shape.radius,)))
self.append("")
if node == "visual":
self.append_material(shape.color)
# 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))}{key}>')
self.append(f"{node}>")
def add_geometries(self, link: Link, part: Part, T_world_link: np.ndarray):
"""
Add a part geometries
"""
shape_n = 0
for shape in part.shapes:
if shape.visual:
shape_n += 1
self.add_shape(link, part, "visual", T_world_link, shape, shape_n)
if shape.collision:
shape_n += 1
self.add_shape(link, part, "collision", T_world_link, shape, shape_n)
mesh_n = 0
for mesh in part.meshes:
if mesh.visual:
mesh_n += 1
self.add_mesh(link, part, "visual", T_world_link, mesh, mesh_n)
if mesh.collision:
mesh_n += 1
self.add_mesh(link, part, "collision", T_world_link, mesh, mesh_n)
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.pose(T_link_joint, relative_to=joint.parent.name))
self.append(f"{joint.parent.name}")
self.append(f"{joint.child.name}")
self.append("")
self.append("%g %g %g" % self.config.round(tuple(joint.axis)))
self.append("")
if "max_effort" in joint.properties:
self.append("%g" % self.config.round(joint.properties["max_effort"]))
else:
self.append("10")
if "max_velocity" in joint.properties:
self.append("%g" % self.config.round(joint.properties["max_velocity"]))
else:
self.append("10")
joint_limits = joint.properties.get("limits", joint.limits)
if joint_limits is None:
if joint_type == "revolute":
joint_limits = [-np.pi, np.pi]
if joint_type == "prismatic":
joint_limits = [-1, 1]
self.append(f"{joint_limits[0]}")
self.append(f"{joint_limits[1]}")
self.append("")
if joint.relation is not None:
self.append(
f'{joint.relation.ratio}'
)
self.append("")
self.append(f"{joint.properties.get('friction', 0.)}")
self.append(f"{joint.properties.get('damping', 0.)}")
self.append("")
self.append("")
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
self.append(f'')
self.append(self.pose(T_link_frame, relative_to=link.name))
self.append("")
def add_link(self, robot: Robot, link: Link, joint: Joint = None):
"""
Adds a link recursively to the SDF file
"""
self.append(f"")
self.append(f'')
T_world_link = np.eye(4)
if joint is not None:
T_world_link = joint.T_world_joint
# Adding inertial properties
mass, com, inertia = link.get_dynamics(T_world_link)
self.add_inertial(mass, com, inertia, link.name)
relative_to = ""
if joint is not None:
relative_to = joint.name
self.append(self.pose(np.eye(4), relative_to=relative_to))
# Adding geometry objects
for part in link.parts:
self.append(f"")
self.add_geometries(link, part, T_world_link)
self.append("")
if link.fixed:
self.append(f'')
self.append(f"world")
self.append(f"{link.name}")
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)
self.add_joint(joint, T_world_link)
def pose(self, matrix: np.ndarray, relative_to: str = ""):
"""
Transforms a transformation matrix into a SDF pose tag
"""
relative = ""
if relative_to:
relative = f' relative_to="{relative_to}"'
sdf = "%g %g %g %g %g %g"
return sdf % (relative, *self.config.round((*matrix[:3, 3], *rotation_matrix_to_rpy(matrix))))
def write_xml(self, robot: Robot, filename: str) -> str:
model_config = MODEL_CONFIG_XML % (robot.name, os.path.basename(filename))
super().write_xml(robot, filename)
dirname = os.path.dirname(filename)
with open(dirname + "/model.config", "w") as file:
file.write(model_config)
print(success(f"* Writing model.config"))