Skip to content

Commit d2cd5f0

Browse files
authored
Merge pull request #681 from compas-dev/urdf_writer
Urdf writer
2 parents 67531bf + 427f57f commit d2cd5f0

File tree

12 files changed

+715
-28
lines changed

12 files changed

+715
-28
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
* Added URDF and XML writers.
13+
* Added `compas.robots.RobotModel.to_urdf_file`.
14+
* Added `compas.files.URDF.from_robot`.
15+
1216
### Changed
17+
* Fixed default value for `compas.robots.Axis`.
1318

1419
* Changed surface to mesh conversion to include cleanup and filter functions, and use the outer loop of all brep faces.
1520

@@ -57,7 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5762
* Fixed bug in `__getstate__`, `__setstate__` of `compas.base.Base`.
5863
* Fixed bug in `compas_rhino.artists.MeshArtist` and `compas_rhino.artists.NetworkArtist`.
5964
* Changed length and force constraints of DR to optional parameters.
60-
* Removed `ABCMeta` from the list of base clases of several objects in compas.
65+
* Removed `ABCMeta` from the list of base classes of several objects in compas.
6166

6267
### Removed
6368

src/compas/files/urdf.py

Lines changed: 127 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88

99
from compas.base import Base
1010
from compas.files.xml import XML
11+
from compas.files.xml import XMLElement
1112
from compas.utilities import memoize
1213

1314
__all__ = [
1415
'URDF',
15-
'URDFParser'
16+
'URDFElement',
17+
'URDFParser',
1618
]
1719

1820

@@ -44,9 +46,30 @@ class URDF(object):
4446
4547
"""
4648

47-
def __init__(self, xml):
49+
def __init__(self, xml=None):
4850
self.xml = xml
49-
self.robot = URDFParser.parse_element(xml.root, xml.root.tag)
51+
self._robot = None
52+
53+
@property
54+
def robot(self):
55+
if self._robot is None:
56+
self._robot = URDFParser.parse_element(self.xml.root, self.xml.root.tag)
57+
return self._robot
58+
59+
@robot.setter
60+
def robot(self, robot):
61+
robot_element = robot.get_urdf_element()
62+
root = robot_element.get_root()
63+
robot_element.add_children(root)
64+
self.xml = XML()
65+
self.xml.root = root
66+
self._robot = robot
67+
68+
@classmethod
69+
def from_robot(cls, robot):
70+
urdf = cls()
71+
urdf.robot = robot
72+
return urdf
5073

5174
@classmethod
5275
def from_file(cls, source):
@@ -80,6 +103,84 @@ def from_string(cls, text):
80103
"""
81104
return cls(XML.from_string(text))
82105

106+
@classmethod
107+
def read(cls, source):
108+
"""Parse a URDF file from a file path or file-like object.
109+
110+
Parameters
111+
----------
112+
source : str or file
113+
File path or file-like object.
114+
115+
Examples
116+
--------
117+
>>> from compas.files import URDF
118+
>>> urdf = URDF.read('/urdf/ur5.urdf')
119+
"""
120+
return cls.from_file(source)
121+
122+
def to_file(self, destination=None, prettify=False):
123+
"""Writes the string representation of this URDF instance,
124+
including all sub-elements, to the ``destination``.
125+
126+
Parameters
127+
----------
128+
destination : str, optional
129+
Filepath where the URDF should be written. Defaults to
130+
the filepath of the associated XML object.
131+
prettify : bool, optional
132+
Whether the string should add whitespace for legibility.
133+
Defaults to ``False``.
134+
135+
Returns
136+
-------
137+
``None``
138+
139+
"""
140+
if destination:
141+
self.xml.filepath = destination
142+
self.xml.write(prettify=prettify)
143+
144+
def to_string(self, encoding='utf-8', prettify=False):
145+
"""Generate a string representation of this URDF instance,
146+
including all sub-elements.
147+
148+
Parameters
149+
----------
150+
encoding : str, optional
151+
Output encoding (the default is 'utf-8')
152+
prettify : bool, optional
153+
Whether the string should add whitespace for legibility.
154+
Defaults to ``False``.
155+
156+
Returns
157+
-------
158+
str
159+
String representation of the URDF.
160+
161+
"""
162+
return self.xml.to_string(encoding=encoding, prettify=prettify)
163+
164+
def write(self, destination=None, prettify=False):
165+
"""Writes the string representation of this URDF instance,
166+
including all sub-elements, to the ``destination``.
167+
168+
Parameters
169+
----------
170+
destination : str, optional
171+
Filepath where the URDF should be written. Defaults to
172+
the filepath of the associated XML object.
173+
prettify : bool, optional
174+
Whether the string should add whitespace for legibility.
175+
Defaults to ``False``.
176+
177+
Returns
178+
-------
179+
``None``
180+
181+
"""
182+
self.to_file(destination=destination, prettify=prettify)
183+
83184

84185
class URDFParser(object):
85186
"""Parse URDF elements into an object graph."""
@@ -107,7 +208,7 @@ def parse_element(cls, element, path=''):
107208
"""Recursively parse URDF element and its children.
108209
109210
If the parser type implements a class method ``from_urdf``,
110-
it will use it to parse the elemenet, otherwise
211+
it will use it to parse the element, otherwise
111212
a generic implementation that relies on conventions
112213
will be used.
113214
@@ -202,6 +303,12 @@ class URDFGenericElement(Base):
202303
"""Generic representation for all URDF elements that
203304
are not explicitly supported."""
204305

306+
def get_urdf_element(self):
307+
if not (hasattr(self, '_urdf_source') or hasattr(self, 'tag')):
308+
raise Exception('No tag found for element {}'.format(self))
309+
tag = self.tag if hasattr(self, 'tag') else self._urdf_source.tag
310+
return URDFElement(tag, self.attr, self.elements, self.text)
311+
205312
@classmethod
206313
def from_urdf(cls, attributes, elements, text):
207314
el = cls()
@@ -246,6 +353,22 @@ def to_json(self, filepath):
246353
json.dump(self.data, f)
247354

248355

356+
class URDFElement(XMLElement):
357+
def __init__(self, tag, attributes=None, elements=None, text=None):
358+
elements = [e.get_urdf_element() for e in elements or [] if e is not None]
359+
super(URDFElement, self).__init__(tag, attributes, elements, text)
360+
self.redistribute_elements()
361+
362+
def redistribute_elements(self):
363+
attributes = {}
364+
for key, value in self.attributes.items():
365+
if hasattr(value, 'get_urdf_element'):
366+
self.elements.append(value.get_urdf_element())
367+
else:
368+
attributes[key] = str(value)
369+
self.attributes = attributes
370+
371+
249372
@memoize
250373
def get_metadata(type):
251374
metadata = dict()

0 commit comments

Comments
 (0)