Source code for ladybug_geometry.interop.stl

# coding=utf-8
"""A class that supports the import and export of STL data to/from ladybug_geometry.

The methods in the object below are inspired from pySTL module.

[1] Daniel Balzerson. 2013. pySTL - Python code for working with .STL
(sterolithography) files. https://github.com/proverbialsunrise/pySTL
"""
import os
import struct
import re

try:
    from itertools import izip as zip  # python 2
    writemode = 'wb'
except ImportError:
    writemode = 'w'  # python 3

from ladybug_geometry.geometry3d.pointvector import Vector3D, Point3D


[docs]class STL(object): """A class that supports the import and export of STL data to/from ladybug_geometry. Args: face_vertices: A list of tuples where each tuple is a triangular face of three Point3Ds. face_normals: A list of Vector3Ds for the normals of the faces in the STL. name: Text string for the name of the solid object in the STL file. (Default: polyhedron). Properties: * name * face_vertices * face_normals """ __slots__ = ('_name', '_face_vertices', '_face_normals') def __init__(self, face_vertices, face_normals, name='polyhedron'): self.name = name self._face_normals = None # bypass check on first time self.face_vertices = face_vertices self.face_normals = face_normals
[docs] @classmethod def from_file(cls, file_path): """Create an STL object from a .stl file. Args: file_path: Path to an STL file as a text string. The STL file can be in either ASCII or binary format. """ face_vertices, face_normals, name = cls._load_stl(file_path) return cls(face_vertices, face_normals, name)
[docs] @classmethod def from_mesh3d(cls, mesh, name='polyhedron'): """Create an STL object from a ladybug_geometry Mesh3D object. All quad faces will be automatically triangulated by using this method. Args: mesh: A ladybug_geometry Mesh3D object to be converted to an OBJ object. name: Text string for the name of the solid object in the STL file. (Default: polyhedron). """ face_vertices, face_normals = [], [] for f, fn in zip(mesh.faces, mesh.face_normals): if len(f) == 3: face_vertices.append(tuple(mesh._vertices[i] for i in f)) face_normals.append(fn) else: # it's a quad mesh to be triangulated vts1 = (mesh._vertices[f[0]], mesh._vertices[f[1]], mesh._vertices[f[2]]) vts2 = (mesh._vertices[f[2]], mesh._vertices[f[3]], mesh._vertices[f[0]]) face_vertices.append(vts1) face_vertices.append(vts2) face_normals.append(fn) face_normals.append(fn) return cls(face_vertices, face_normals, name)
@property def name(self): """Get the name of the solid object in the STL file.""" return self._name @name.setter def name(self, value): input_name = 'STL object name' try: non_ascii = tuple(i for i in value if ord(i) >= 128) except TypeError: raise TypeError('Input {} must be a text string. Got {}: {}.'.format( input_name, type(value), value)) assert non_ascii == (), 'Illegal characters {} found in {}'.format( non_ascii, input_name) illegal_match = re.search(r'[,;!\n\t]', value) assert illegal_match is None, 'Illegal character "{}" found in {}'.format( illegal_match.group(0), input_name) assert len(value) > 0, 'Input {} "{}" contains no characters.'.format( input_name, value) assert len(value) <= 80, 'Input {} "{}" must be less than 80 characters.'.format( input_name, value) self._name = value @property def face_vertices(self): """Get a list of tuples where each tuple is a triangular face of three Point3Ds. """ return self._face_vertices @face_vertices.setter def face_vertices(self, val): assert isinstance(val, (list, tuple)), \ 'face_vertices should be a list or tuple. Got {}'.format(type(val)) if isinstance(val, list): val = tuple(val) for f in val: assert len(f) == 3, 'All face_vertices of an STL must be triangles. ' \ 'Got face of length {}.'.format(len(f)) self._face_vertices = val self._check_faces_match() @property def face_normals(self): """Get a list of Vector3Ds for the normals of the faces in the STL.""" return self._face_normals @face_normals.setter def face_normals(self, val): assert isinstance(val, (list, tuple)), \ 'face_normals should be a list or tuple. Got {}'.format(type(val)) if isinstance(val, list): val = tuple(val) self._face_normals = val self._check_faces_match()
[docs] def to_file(self, folder, name=None): """Write the STL object to an ASCII STL file. Args: folder: A text string for the directory where the STL will be written. name: A text string for the name of the STL file. If None, the name of the STL object will be used. (Default: None). """ # set up a name and folder if name is None: name = self.name file_name = name if name.lower().endswith('.stl') else '{}.stl'.format(name) stl_file = os.path.join(folder, file_name) # loop through the faces and normals to write them to the file with open(stl_file, writemode) as fp: fp.write('solid {:s}\n'.format(self.name)) for facet, nm in zip(self.face_vertices, self.face_normals): fp.write( ' facet normal {0:.6E} {1:.6E} {2:.6E}\n'.format(nm.x, nm.y, nm.z) ) fp.write(' outer loop\n') for pt in facet: fp.write( ' vertex {0:.6E} {1:.6E} {2:.6E}\n'.format(pt.x, pt.y, pt.z) ) fp.write(' endloop\n') fp.write(' endfacet\n') fp.write('endsolid {:s}\n'.format(self.name)) return stl_file
def _check_faces_match(self): if self._face_normals is not None: assert len(self._face_vertices) == len(self._face_normals), \ 'Number of STL face_vertices ({}) does not match the number ' \ 'of _face_normals ({}).'.format( len(self._face_vertices), len(self._face_normals)) @staticmethod def _load_stl(file_path): """Load data from an STL file. Args: file_path: Path to an STL file as a text string. The STL file can be in either ASCII or binary format. Returns: A tuple with three elements. - face_vertices: A list of tuples where each tuple is a triangular face of three Point3Ds. - face_normals: A list of Vector3Ds for the normals of the faces in the STL. - name: Text string for the name of the STL object """ # check the first bytes of the file to determine whether it's ASCII or binary assert os.path.isfile(file_path), 'Failed to find %s' % file_path with open(file_path, 'rb') as fp: header = fp.read(80) # 80 characters should have the full name first_word = header[0:5] # load the STL data depending on whether it is ASCII or binary if first_word.decode('utf-8') == 'solid': return STL._load_text_stl(file_path) else: return STL._load_binary_stl(file_path) @staticmethod def _load_text_stl(file_path): """Read text stl file and extract triangular faces.""" _face_vertices, _face_normals, _name = [], [], 'polyhedron' with open(file_path, 'r') as fp: for line in fp: words = line.split() if len(words) > 0: first_word = words[0] if first_word == 'facet': # start of a new face vertices = [] norm = Vector3D( float(words[2]), float(words[3]), float(words[4]) ) _face_normals.append(norm) elif first_word == 'vertex': # vertex of a face vertices.append( Point3D(float(words[1]), float(words[2]), float(words[3])) ) elif first_word == 'endloop': # end of a face _face_vertices.append(tuple(vertices)) elif first_word == 'solid': # very start of the file try: _name = words[1] except IndexError: # no name assigned; leave the default one pass return _face_vertices, _face_normals, _name @staticmethod def _load_binary_stl(file_path): """Read binary stl file and extract triangular faces.""" _face_vertices, _face_normals, _name = [], [], 'polyhedron' with open(file_path, 'rb') as fp: # interpret the 80-character header as the name of the object _name = fp.read(80).decode('utf-8').strip() # ignore the total face count in the first 4 characters struct.unpack('I', fp.read(4))[0] # loop through the file contents and load all vertices and vectors count = 0 while True: try: # read the face normal p = fp.read(12) if len(p) == 12: norm = Vector3D( struct.unpack('f', p[0:4])[0], struct.unpack('f', p[4:8])[0], struct.unpack('f', p[8:12])[0] ) else: break # read the first vertex p = fp.read(12) if len(p) == 12: p1 = Point3D( struct.unpack('f', p[0:4])[0], struct.unpack('f', p[4:8])[0], struct.unpack('f', p[8:12])[0] ) else: break # read the second vertex p = fp.read(12) if len(p) == 12: p2 = Point3D( struct.unpack('f', p[0:4])[0], struct.unpack('f', p[4:8])[0], struct.unpack('f', p[8:12])[0] ) else: break # read the third vertex p = fp.read(12) if len(p) == 12: p3 = Point3D( struct.unpack('f', p[0:4])[0], struct.unpack('f', p[4:8])[0], struct.unpack('f', p[8:12])[0] ) else: break # add the triangle to the face vertices _face_vertices.append((p1, p2, p3)) _face_normals.append(norm) count += 1 fp.read(2) if len(p) == 0: break # no more points to read except EOFError: break # we have reached the end of the file return _face_vertices, _face_normals, _name def __len__(self): return len(self._face_vertices) def __getitem__(self, key): return self._face_vertices[key] def __iter__(self): return iter(self._face_vertices) def __repr__(self): return 'STL ({} faces)'.format(len(self._face_vertices))