|
6 | 6 | import logging |
7 | 7 | import os |
8 | 8 | import tempfile |
| 9 | +from collections import OrderedDict |
9 | 10 |
|
10 | 11 | import roslibpy |
11 | 12 | from compas.datastructures import Mesh |
12 | | -from compas.datastructures import meshes_join |
| 13 | +from compas.datastructures import mesh_transform |
13 | 14 | from compas.files import XML |
| 15 | +from compas.geometry import Transformation |
14 | 16 | from compas.robots.resources.basic import _get_file_format |
15 | 17 | from compas.robots.resources.basic import _mesh_import |
16 | 18 | from compas.utilities import await_callback |
| 19 | +from compas.utilities import geometric_key |
17 | 20 |
|
18 | 21 | LOGGER = logging.getLogger('compas_fab.backends.ros') |
19 | 22 |
|
@@ -61,14 +64,17 @@ class RosFileServerLoader(object): |
61 | 64 | local_cache_directory : str, optional |
62 | 65 | Directory name to store the cached files. Only used if |
63 | 66 | ``local_cache`` is ``True``. Defaults to ``~/robot_description``. |
| 67 | + precision : float |
| 68 | + Defines precision for importing/loading meshes. Defaults to ``compas.PRECISION``. |
64 | 69 | """ |
65 | 70 |
|
66 | | - def __init__(self, ros=None, local_cache=False, local_cache_directory=None): |
| 71 | + def __init__(self, ros=None, local_cache=False, local_cache_directory=None, precision=None): |
67 | 72 | self.robot_name = None |
68 | 73 | self.schema_prefix = 'package://' |
69 | 74 | self.ros = ros |
70 | 75 | self.local_cache_directory = None |
71 | 76 | self.local_cache_enabled = local_cache |
| 77 | + self.precision = precision |
72 | 78 |
|
73 | 79 | if self.local_cache_enabled: |
74 | 80 | self.local_cache_directory = local_cache_directory or os.path.join( |
@@ -184,7 +190,7 @@ def load_mesh(self, url): |
184 | 190 |
|
185 | 191 | Returns |
186 | 192 | ------- |
187 | | - :class:`Mesh` |
| 193 | + :class:`Mesh` or list of :class:`Mesh` |
188 | 194 | Instance of a mesh. |
189 | 195 | """ |
190 | 196 | use_local_file = False |
@@ -217,61 +223,85 @@ def load_mesh(self, url): |
217 | 223 | # Nothing to do here, the file will be read by the mesh importer |
218 | 224 | LOGGER.debug('Loading mesh file %s from local cache dir', local_filename) |
219 | 225 |
|
220 | | - return _fileserver_mesh_import(url, local_filename) |
| 226 | + return _fileserver_mesh_import(url, local_filename, self.precision) |
221 | 227 |
|
222 | 228 | def _local_mesh_filename(self, url): |
223 | 229 | return os.path.abspath(os.path.join(self._robot_resource_path, url[len('package://'):])) |
224 | 230 |
|
225 | 231 |
|
226 | | -def _dae_mesh_importer(filename): |
227 | | - """This is a very simple implementation of a DAE/Collada parser. |
228 | | - It merges all solids of the DAE file into one mesh, because |
229 | | - several other parts of the framework don't support multi-meshes per file.""" |
| 232 | +def _dae_mesh_importer(filename, precision): |
| 233 | + """This is a very simple implementation of a DAE/Collada parser.""" |
230 | 234 | dae = XML.from_file(filename) |
231 | 235 | meshes = [] |
| 236 | + visual_scenes = dae.root.find('library_visual_scenes') |
232 | 237 |
|
233 | | - for mesh_xml in dae.root.findall('.//mesh'): |
234 | | - for triangle_set in mesh_xml.findall('triangles'): |
235 | | - triangle_set_data = triangle_set.find('p').text.split() |
| 238 | + for geometry in dae.root.findall('.//geometry'): |
| 239 | + mesh_xml = geometry.find('mesh') |
| 240 | + mesh_id = geometry.attrib['id'] |
| 241 | + matrix_node = visual_scenes.find('visual_scene/node/instance_geometry[@url="#{}"]/../matrix'.format(mesh_id)) |
| 242 | + transform = None |
236 | 243 |
|
237 | | - # Parse vertices |
238 | | - vertices_input = triangle_set.find('input[@semantic="VERTEX"]') |
239 | | - vertices_link = mesh_xml.find('vertices[@id="{}"]/input'.format(vertices_input.attrib['source'][1:])) |
240 | | - positions = mesh_xml.find('source[@id="{}"]/float_array'.format(vertices_link.attrib['source'][1:])) |
241 | | - positions = positions.text.split() |
| 244 | + if matrix_node is not None: |
| 245 | + M = [float(i) for i in matrix_node.text.split()] |
242 | 246 |
|
243 | | - vertices = list(map(float, positions[i:i + 3]) for i in range(0, len(positions), 3)) |
| 247 | + # If it's the identity matrix, then ignore, we don't need to transform it |
| 248 | + if M != [1., 0., 0., 0., |
| 249 | + 0., 1., 0., 0., |
| 250 | + 0., 0., 1., 0., |
| 251 | + 0., 0., 0., 1.]: |
| 252 | + M = M[0:4], M[4:8], M[8:12], M[12:16] |
| 253 | + transform = Transformation.from_matrix(M) |
244 | 254 |
|
245 | | - # Parse faces |
246 | | - faces = list(map(int, triangle_set_data[::2])) # Ignore normals (ever second item is normal index) |
247 | | - faces = list(faces[i:i + 3] for i in range(0, len(faces), 3)) |
| 255 | + triangle_set = mesh_xml.find('triangles') |
| 256 | + triangle_set_data = triangle_set.find('p').text.split() |
248 | 257 |
|
249 | | - mesh = Mesh.from_vertices_and_faces(vertices, faces) |
| 258 | + # Parse vertices |
| 259 | + vertices_input = triangle_set.find('input[@semantic="VERTEX"]') |
| 260 | + vertices_link = mesh_xml.find('vertices[@id="{}"]/input'.format(vertices_input.attrib['source'][1:])) |
| 261 | + positions = mesh_xml.find('source[@id="{}"]/float_array'.format(vertices_link.attrib['source'][1:])) |
| 262 | + positions = positions.text.split() |
250 | 263 |
|
251 | | - meshes.append(mesh) |
| 264 | + vertices = [[float(p) for p in positions[i:i + 3]] for i in range(0, len(positions), 3)] |
252 | 265 |
|
253 | | - combined_mesh = meshes_join(meshes) |
| 266 | + # Parse faces |
| 267 | + faces = [int(f) for f in triangle_set_data[::2]] # Ignore normals (every second item is normal index) |
| 268 | + faces = [faces[i:i + 3] for i in range(0, len(faces), 3)] |
254 | 269 |
|
255 | | - # from compas.datastructures import mesh_transform |
256 | | - # from compas.geometry import Frame |
257 | | - # from compas.geometry import Transformation |
| 270 | + # Rebuild vertices and faces using the same logic that other importers |
| 271 | + # use remapping everything based on a selected precision |
| 272 | + index_key = OrderedDict() |
| 273 | + vertex = OrderedDict() |
258 | 274 |
|
259 | | - # former DAE files have yaxis and zaxis swapped |
260 | | - # frame = Frame([0, 0, 0], [1, 0, 0], [0, 0, 1]) |
261 | | - # T = Transformation.from_frame(frame) |
262 | | - # mesh_transform(combined_mesh, T) |
263 | | - return combined_mesh |
| 275 | + for i, xyz in enumerate(vertices): |
| 276 | + key = geometric_key(xyz, precision) |
| 277 | + index_key[i] = key |
| 278 | + vertex[key] = xyz |
264 | 279 |
|
| 280 | + key_index = {key: index for index, key in enumerate(vertex)} |
| 281 | + index_index = {index: key_index[key] for index, key in iter(index_key.items())} |
| 282 | + vertices = [xyz for xyz in iter(vertex.values())] |
| 283 | + faces = [[index_index[index] for index in face] for face in faces] |
265 | 284 |
|
266 | | -def _fileserver_mesh_import(url, filename): |
| 285 | + mesh = Mesh.from_vertices_and_faces(vertices, faces) |
| 286 | + |
| 287 | + if transform: |
| 288 | + mesh_transform(mesh, transform) |
| 289 | + |
| 290 | + meshes.append(mesh) |
| 291 | + |
| 292 | + return meshes |
| 293 | + |
| 294 | + |
| 295 | +def _fileserver_mesh_import(url, filename, precision=None): |
267 | 296 | """Internal function that adds primitive support for DAE files |
268 | 297 | to the _mesh_import function of compas.robots.""" |
269 | 298 | file_extension = _get_file_format(url) |
270 | 299 |
|
271 | 300 | if file_extension == 'dae': |
272 | 301 | # Magic! |
273 | | - return _dae_mesh_importer(filename) |
| 302 | + return _dae_mesh_importer(filename, precision) |
274 | 303 | else: |
| 304 | + # TODO: This _mesh_import should also add support for precision |
275 | 305 | return _mesh_import(url, filename) |
276 | 306 |
|
277 | 307 |
|
|
0 commit comments