33from __future__ import print_function
44
55from collections import OrderedDict
6+ from collections import defaultdict
67
78import compas
89from compas import _iotools
1920class 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
75136class 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
376464class 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