Skip to content
Snippets Groups Projects
stl_utils.py 7.34 KiB
Newer Older
  • Learn to ignore specific revisions
  • # SPDX-License-Identifier: GPL-2.0-or-later
    
    Campbell Barton's avatar
    Campbell Barton committed
    
    
    Import and export STL files
    
    Used as a blender script, it load all the stl files in the scene:
    
    
    blender --python stl_utils.py -- file1.stl file2.stl file3.stl ...
    
    # TODO: endian
    
    class ListDict(dict):
    
        Set struct with order.
    
        You can:
           - insert data into without doubles
           - get the list of data in insertion order with self.list
    
        Like collections.OrderedDict, but quicker, can be replaced if
        ODict is optimised.
    
    
        def __init__(self):
            dict.__init__(self)
            self.list = []
            self._len = 0
    
        def add(self, item):
    
            Add a value to the Set, return its position in it.
    
            value = self.setdefault(item, self._len)
            if value == self._len:
                self.list.append(item)
                self._len += 1
    
            return value
    
    
    
    # an stl binary file is
    # - 80 bytes of description
    # - 4 bytes of size (unsigned int)
    # - size triangles :
    #
    #   - 12 bytes of normal
    #   - 9 * 4 bytes of coordinate (3*3 floats)
    #   - 2 bytes of garbage (usually 0)
    
    BINARY_HEADER = 80
    BINARY_STRIDE = 12 * 4 + 2
    
    
    def _header_version():
        import bpy
        return "Exported from Blender-" + bpy.app.version_string
    
    
    
        This function returns True if the data represents an ASCII file.
    
        Please note that a False value does not necessary means that the data
        represents a binary file. It can be a (very *RARE* in real life, but
        can easily be forged) ascii file.
    
    
        import os
        import struct
    
    
        data.seek(BINARY_HEADER)
        size = struct.unpack('<I', data.read(4))[0]
        # Use seek() method to get size of the file.
        data.seek(0, os.SEEK_END)
        file_size = data.tell()
        # Reset to the start of the file.
        data.seek(0)
    
        if size == 0:  # Odds to get that result from an ASCII file are null...
            print("WARNING! Reported size (facet number) is 0, assuming invalid binary STL file.")
            return False  # Assume binary in this case.
    
    
        return (file_size != BINARY_HEADER + 4 + BINARY_STRIDE * size)
    
    def _binary_read(data):
    
    
        import os
        import struct
    
    
        data.seek(BINARY_HEADER)
        size = struct.unpack('<I', data.read(4))[0]
    
    
        if size == 0:
            # Workaround invalid crap.
            data.seek(0, os.SEEK_END)
            file_size = data.tell()
            # Reset to after-the-size in the file.
            data.seek(BINARY_HEADER + 4)
    
            file_size -= BINARY_HEADER + 4
            size = file_size // BINARY_STRIDE
            print("WARNING! Reported size (facet number) is 0, inferring %d facets from file size." % size)
    
    
        # We read 4096 elements at once, avoids too much calls to read()!
        CHUNK_LEN = 4096
        chunks = [CHUNK_LEN] * (size // CHUNK_LEN)
        chunks.append(size % CHUNK_LEN)
    
        unpack = struct.Struct('<12f').unpack_from
    
        for chunk_len in chunks:
            if chunk_len == 0:
                continue
            buf = data.read(BINARY_STRIDE * chunk_len)
            for i in range(chunk_len):
    
                # read the normal and points coordinates of each triangle
                pt = unpack(buf, BINARY_STRIDE * i)
                yield pt[:3], (pt[3:6], pt[6:9], pt[9:])
    
    
    
    def _ascii_read(data):
        # an stl ascii file is like
        # HEADER: solid some name
        # for each face:
        #
        #     facet normal x y z
        #     outerloop
        #          vertex x y z
        #          vertex x y z
        #          vertex x y z
        #     endloop
        #     endfacet
    
        # strip header
        data.readline()
    
    
            if l.startswith(b'facet'):
    
    Bastien Montagne's avatar
    Bastien Montagne committed
                curr_nor = tuple(map(float, l.split()[2:]))
    
            # if we encounter a vertex, read next 2
    
            if l.startswith(b'vertex'):
    
                yield curr_nor, [tuple(map(float, l_item.split()[1:])) for l_item in (l, data.readline(), data.readline())]
    
    def _binary_write(filepath, faces):
    
        import struct
        import itertools
        from mathutils.geometry import normal
    
    
        with open(filepath, 'wb') as data:
            fw = data.write
    
    Campbell Barton's avatar
    Campbell Barton committed
            # we write padding at header beginning to avoid to
    
            # call len(list(faces)) which may be expensive
    
            fw(struct.calcsize('<80sI') * b'\0')
    
    
            # 3 vertex == 9f
            pack = struct.Struct('<9f').pack
    
    
            # number of vertices written
    
            for face in faces:
                # calculate face normal
    
                # write normal + vertices + pad as attributes
    
                fw(struct.pack('<3f', *normal(*face)) + pack(*itertools.chain.from_iterable(face)))
                # attribute byte count (unused)
    
                fw(b'\0\0')
    
    
            # header, with correct value now
            data.seek(0)
    
            fw(struct.pack('<80sI', _header_version().encode('ascii'), nb))
    
    def _ascii_write(filepath, faces):
    
        from mathutils.geometry import normal
    
    
        with open(filepath, 'w') as data:
            fw = data.write
    
            fw('solid %s\n' % header)
    
    
            for face in faces:
                # calculate face normal
                fw('facet normal %f %f %f\nouter loop\n' % normal(*face)[:])
                for vert in face:
                    fw('vertex %f %f %f\n' % vert[:])
                fw('endloop\nendfacet\n')
    
    def write_stl(filepath="", faces=(), ascii=False):
    
        Write a stl file from faces,
    
    
        filepath
           output filepath
    
    
        faces
           iterable of tuple of 3 vertex, vertex is tuple of 3 coordinates as float
    
        ascii
           save the file in ascii format (very huge)
    
        (_ascii_write if ascii else _binary_write)(filepath, faces)
    
    def read_stl(filepath):
    
        Return the triangles and points of an stl binary file.
    
        Please note that this process can take lot of time if the file is
        huge (~1m30 for a 1 Go stl file on an quad core i7).
    
    
        - returns a tuple(triangles, triangles' normals, points).
    
    
          triangles
              A list of triangles, each triangle as a tuple of 3 index of
              point in *points*.
    
    
          triangles' normals
              A list of vectors3 (tuples, xyz).
    
    
          points
              An indexed list of points, each point is a tuple of 3 float
              (xyz).
    
        Example of use:
    
    
           >>> tris, tri_nors, pts = read_stl(filepath)
    
           >>> pts = list(pts)
           >>>
           >>> # print the coordinate of the triangle n
    
           >>> print(pts[i] for i in tris[n])
    
        import time
        start_time = time.process_time()
    
        tris, tri_nors, pts = [], [], ListDict()
    
            # check for ascii or binary
    
            gen = _ascii_read if _is_ascii_file(data) else _binary_read
    
            for nor, pt in gen(data):
    
                # Add the triangle and the point.
    
                # If the point is already in the list of points, the
    
                # index returned by pts.add() will be the one from the
                # first equal point inserted.
    
                tris.append([pts.add(p) for p in pt])
    
        print('Import finished in %.4f sec.' % (time.process_time() - start_time))
    
    
        return tris, tri_nors, pts.list
    
    
    
    if __name__ == '__main__':
        import sys
        import bpy
        from io_mesh_stl import blender_utils
    
    
        filepaths = sys.argv[sys.argv.index('--') + 1:]
    
        for filepath in filepaths:
            objName = bpy.path.display_name(filepath)
            tris, pts = read_stl(filepath)
    
    
            blender_utils.create_and_link_mesh(objName, tris, pts)