Skip to content

Commit dba1c61

Browse files
authored
Merge pull request #877 from compas-dev/obj-objects-groups
Support for named objects in OBJ reading/writing
2 parents e3ddc21 + cfe4850 commit dba1c61

File tree

3 files changed

+152
-54
lines changed

3 files changed

+152
-54
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
* Added equality comparison for pointclouds.
1414
* Added `compas.data.is_sequence_of_uint`.
1515
* Added general plotter for geometry objects and data structures based on the artist registration mechanism.
16+
* Added support for multimesh files to OBJ reader/writer.
1617

1718
### Changed
1819

src/compas/datastructures/mesh/core/mesh.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ def from_vertices_and_faces(cls, vertices, faces):
395395

396396
if isinstance(vertices, mapping):
397397
for key, xyz in vertices.items():
398-
mesh.add_vertex(key=key, attr_dict={i: j for i, j in zip(['x', 'y', 'z'], xyz)})
398+
mesh.add_vertex(key=key, attr_dict=dict(zip(('x', 'y', 'z'), xyz)))
399399
else:
400400
for x, y, z in iter(vertices):
401401
mesh.add_vertex(x=x, y=y, z=z)

src/compas/files/obj.py

Lines changed: 150 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import print_function
44

55
from collections import OrderedDict
6+
from collections import defaultdict
67

78
import compas
89
from compas import _iotools
@@ -19,6 +20,58 @@
1920
class OBJ(object):
2021
"""Read and write files in OBJ format.
2122
23+
Currently, reading is only supported for polygonal geometry.
24+
Writing is only supported for meshes.
25+
26+
Examples
27+
--------
28+
Reading and writing of a single mesh.
29+
30+
.. code-block:: python
31+
32+
from compas.datastructures import Mesh
33+
from compas.files import OBJ
34+
35+
mesh = Mesh.from_polyhedron(12)
36+
37+
# write to file
38+
obj = OBJ('mesh.obj')
39+
obj.write(mesh)
40+
41+
# read from file
42+
obj = OBJ('mesh.obj')
43+
obj.read()
44+
45+
mesh = Mesh.from_vertices_and_faces(obj.vertices, obj.faces)
46+
47+
Reading and writing of multiple meshes as separate objects in a single OBJ file.
48+
49+
.. code-block:: python
50+
51+
from compas.geometry import Pointcloud, Translation
52+
from compas.datastructures import Mesh
53+
from compas.files import OBJ
54+
55+
meshes = []
56+
for point in Pointcloud.from_bounds(10, 10, 10, 100):
57+
mesh = Mesh.from_polyhedron(12)
58+
mesh.transform(Translation.from_vector(point))
59+
meshes.append(mesh)
60+
61+
# write to file
62+
obj = OBJ('meshes.obj')
63+
obj.write(meshes)
64+
65+
# read from file
66+
obj = OBJ('meshes.obj')
67+
obj.read()
68+
69+
meshes = []
70+
for name in obj.objects:
71+
mesh = Mesh.from_vertices_and_faces(* obj.objects[name])
72+
mesh.name = name
73+
meshes.append(mesh)
74+
2275
References
2376
----------
2477
.. [1] http://paulbourke.net/dataformats/obj/
@@ -71,6 +124,14 @@ def lines(self):
71124
def faces(self):
72125
return self.parser.faces
73126

127+
@property
128+
def objects(self):
129+
return self.parser.objects
130+
131+
@property
132+
def groups(self):
133+
return self.parser.groups
134+
74135

75136
class OBJReader(object):
76137
"""Read the contents of an *obj* file.
@@ -102,6 +163,10 @@ class OBJReader(object):
102163
Curves
103164
surfaces : list
104165
Surfaces
166+
objects : dict
167+
The named objects.
168+
groups : dict
169+
The named polygon groups.
105170
106171
Notes
107172
-----
@@ -138,9 +203,10 @@ def __init__(self, filepath):
138203
# free-form statements
139204
# parm, trim, hole, scrv, sp, end
140205
# grouping
141-
self.groups = {}
142-
self.objects = {}
206+
self.groups = defaultdict(list)
207+
self.objects = defaultdict(list)
143208
self.group = None
209+
self.object = None
144210

145211
def open(self):
146212
with _iotools.open_file(self.filepath, 'r') as f:
@@ -191,6 +257,8 @@ def read(self):
191257
* ``bmat``: freeform attribute *basis matrix*
192258
* ``step``: freeform attribute *step size*
193259
* ``cstype``: freeform attribute *curve or surface type*
260+
* ``o``: start of named object
261+
* ``g``: start of a named group
194262
195263
"""
196264
if not self.content:
@@ -268,15 +336,17 @@ def _read_polygonal_geometry(self, name, data):
268336
# point
269337
if name == 'p':
270338
self.points.append(int(data[0]) - 1)
271-
if self.group:
272-
self.groups[self.group].append(('p', len(self.points) - 1))
339+
ref = 'p', len(self.points) - 1
340+
self.groups[self.group].append(ref)
341+
self.objects[self.object].append(ref)
273342
# line
274343
elif name == 'l':
275344
if len(data) < 2:
276345
return
277346
self.lines.append([int(i) - 1 for i in data])
278-
if self.group:
279-
self.groups[self.group].append(('l', len(self.lines) - 1))
347+
ref = 'l', len(self.lines) - 1
348+
self.groups[self.group].append(ref)
349+
self.objects[self.object].append(ref)
280350
# face
281351
elif name == 'f':
282352
if len(data) < 3:
@@ -287,8 +357,9 @@ def _read_polygonal_geometry(self, name, data):
287357
i = int(parts[0]) - 1
288358
face.append(i)
289359
self.faces.append(face)
290-
if self.group:
291-
self.groups[self.group].append(('f', len(self.faces) - 1))
360+
ref = 'f', len(self.faces) - 1
361+
self.groups[self.group].append(ref)
362+
self.objects[self.object].append(ref)
292363

293364
def _read_freeform_attribute(self, name, data):
294365
if name == 'deg':
@@ -313,22 +384,29 @@ def _read_freeform_geometry(self, name, data):
313384
if self.deg[0] == 1:
314385
if len(data) == 4:
315386
self.lines.append((int(data[2]) - 1, int(data[3]) - 1))
316-
if self.group:
317-
self.groups[self.group].append(('l', len(self.lines) - 1))
387+
ref = 'l', len(self.lines) - 1
388+
self.groups[self.group].append(ref)
389+
self.objects[self.object].append(ref)
318390
return
319391
if len(data) > 4:
320392
self.lines.append([int(d) - 1 for d in data[2:]])
321-
# if self.group:
322-
# self.groups[self.group].append(('l', len(self.lines) - 1))
393+
ref = 'l', len(self.lines) - 1
394+
self.groups[self.group].append(ref)
395+
self.objects[self.object].append(ref)
323396
return
324397

325398
def _read_freeform_statement(self, name, data):
326399
pass
327400

328401
def _read_grouping(self, name, data):
402+
if name == 'o':
403+
self.object = ' '.join(data)
404+
self.objects[self.object] = []
405+
return
329406
if name == 'g':
330-
self.group = data[0]
407+
self.group = ' '.join(data)
331408
self.groups[self.group] = []
409+
self.objects[self.object].append(('g', self.group))
332410
return
333411

334412

@@ -351,7 +429,6 @@ def __init__(self, reader, precision=None):
351429
self.surfaces = None
352430
self.groups = None
353431
self.objects = None
354-
# self.parse()
355432

356433
def parse(self):
357434
index_key = OrderedDict()
@@ -371,69 +448,89 @@ def parse(self):
371448
self.polylines = [[index_index[index] for index in line] for line in self.reader.lines if len(line) > 2]
372449
self.faces = [[index_index[index] for index in face] for face in self.reader.faces]
373450
self.groups = self.reader.groups
451+
self.objects = {}
452+
for name in self.reader.objects:
453+
faces = []
454+
for item in self.reader.objects[name]:
455+
if item[0] == 'f':
456+
faces.append(self.faces[item[1]])
457+
vertices = {}
458+
for face in faces:
459+
for vertex in face:
460+
vertices[vertex] = self.vertices[vertex]
461+
self.objects[name] = vertices, faces
374462

375463

376464
class OBJWriter(object):
377465

378-
def __init__(self, filepath, mesh, precision=None, unweld=False, author=None, email=None, date=None):
466+
def __init__(self, filepath, meshes, precision=None, unweld=False, author=None, email=None, date=None):
379467
self.filepath = filepath
380-
self.mesh = mesh
468+
self.meshes = meshes if isinstance(meshes, (list, tuple)) else [meshes]
381469
self.author = author
382470
self.email = email
383471
self.date = date
384472
self.precision = precision or compas.PRECISION
385473
self.unweld = unweld
386474
self.vertex_tpl = "v {0:." + self.precision + "}" + " {1:." + self.precision + "}" + " {2:." + self.precision + "}\n"
387-
self.v = mesh.number_of_vertices()
388-
self.f = mesh.number_of_faces()
389-
self.e = mesh.number_of_edges()
475+
self.v = sum(mesh.number_of_vertices() for mesh in self.meshes)
476+
self.f = sum(mesh.number_of_faces() for mesh in self.meshes)
477+
self.e = sum(mesh.number_of_edges() for mesh in self.meshes)
478+
self._v = 1
390479
self.file = None
391480

392481
def write(self):
393482
with _iotools.open_file(self.filepath, 'w') as self.file:
394483
self.write_header()
395-
if self.unweld:
396-
self.write_vertices_and_faces()
397-
else:
398-
self.write_vertices()
399-
self.write_faces()
484+
self.write_meshes()
400485

401486
def write_header(self):
402-
self.file.write("# OBJ\n")
403-
self.file.write("# COMPAS\n")
404-
self.file.write("# version: {}\n".format(compas.__version__))
405-
self.file.write("# precision: {}\n".format(self.precision))
406-
self.file.write("# V F E: {} {} {}\n".format(self.v, self.f, self.e))
487+
self.file.write('# OBJ\n')
488+
self.file.write('# COMPAS\n')
489+
self.file.write('# version: {}\n'.format(compas.__version__))
490+
self.file.write('# precision: {}\n'.format(self.precision))
491+
self.file.write('# V F E: {} {} {}\n'.format(self.v, self.f, self.e))
407492
if self.author:
408-
self.file.write("# author: {}\n".format(self.author))
493+
self.file.write('# author: {}\n'.format(self.author))
409494
if self.email:
410-
self.file.write("# email: {}\n".format(self.email))
495+
self.file.write('# email: {}\n'.format(self.email))
411496
if self.date:
412-
self.file.write("# date: {}\n".format(self.date))
413-
self.file.write("\n")
497+
self.file.write('# date: {}\n'.format(self.date))
498+
self.file.write('\n')
499+
500+
def write_meshes(self):
501+
for index, mesh in enumerate(self.meshes):
502+
name = mesh.name
503+
if name == 'Mesh':
504+
name = 'Mesh {}'.format(index)
505+
self.file.write('o {}\n'.format(name))
506+
if self.unweld:
507+
self._write_vertices_and_faces(mesh)
508+
else:
509+
self._write_vertices(mesh)
510+
self._write_faces(mesh)
511+
self._v += mesh.number_of_vertices()
414512

415-
def write_vertices(self):
416-
for key in self.mesh.vertices():
417-
x, y, z = self.mesh.vertex_coordinates(key)
513+
def _write_vertices(self, mesh):
514+
for key in mesh.vertices():
515+
x, y, z = mesh.vertex_coordinates(key)
418516
self.file.write(self.vertex_tpl.format(x, y, z))
419517

420-
def write_faces(self):
421-
key_index = self.mesh.key_index()
422-
for fkey in self.mesh.faces():
423-
vertices = self.mesh.face_vertices(fkey)
424-
vertices = [key_index[key] + 1 for key in vertices]
425-
vertices_str = " ".join([str(index) for index in vertices])
426-
self.file.write("f {0}\n".format(vertices_str))
427-
428-
def write_vertices_and_faces(self):
429-
index = 1
430-
for face in self.mesh.faces():
431-
vertices = self.mesh.face_vertices(face)
518+
def _write_faces(self, mesh):
519+
key_index = mesh.key_index()
520+
for fkey in mesh.faces():
521+
vertices = mesh.face_vertices(fkey)
522+
vertices = [key_index[key] + self._v for key in vertices]
523+
vertices_str = ' '.join([str(index) for index in vertices])
524+
self.file.write('f {0}\n'.format(vertices_str))
525+
526+
def _write_vertices_and_faces(self, mesh):
527+
for face in mesh.faces():
528+
vertices = mesh.face_vertices(face)
432529
indices = []
433530
for vertex in vertices:
434-
x, y, z = self.mesh.vertex_coordinates(vertex)
531+
x, y, z = mesh.vertex_coordinates(vertex)
435532
self.file.write(self.vertex_tpl.format(x, y, z))
436-
indices.append(index)
437-
index += 1
438-
indices_str = " ".join([str(i) for i in indices])
439-
self.file.write("f {0}\n".format(indices_str))
533+
indices.append(self._v)
534+
self._v += 1
535+
indices_str = ' '.join([str(i) for i in indices])
536+
self.file.write('f {0}\n'.format(indices_str))

0 commit comments

Comments
 (0)