diff options
Diffstat (limited to 'io_mesh_stl')
-rw-r--r-- | io_mesh_stl/__init__.py | 155 | ||||
-rw-r--r-- | io_mesh_stl/blender_utils.py | 50 | ||||
-rw-r--r-- | io_mesh_stl/stl_utils.py | 228 |
3 files changed, 433 insertions, 0 deletions
diff --git a/io_mesh_stl/__init__.py b/io_mesh_stl/__init__.py new file mode 100644 index 00000000..f04cc618 --- /dev/null +++ b/io_mesh_stl/__init__.py @@ -0,0 +1,155 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +''' +Import/Export STL files (binary or ascii) + +- Import automatically remove the doubles. +- Export can export with/without modifiers applied + +Issues: + +Import: + - Does not handle the normal of the triangles + - Does not handle endien + +Export: + - Does not do the object space transformation + - Export only one object (the selected one) +''' + +bl_addon_info = { + 'name': 'I/O: STL', + 'author': 'Guillaume Bouchard (Guillaum)', + 'version': '1', + 'blender': (2, 5, 3), + 'location': 'File > Import/Export > Stl', + 'description': 'Import/Export Stl files', + 'url': '...', + 'category': 'Import/Export'} + +import bpy +from bpy.props import * + +from io_mesh_stl import stl_utils, blender_utils + + +class StlImporter(bpy.types.Operator): + ''' + Load STL triangle mesh data + ''' + bl_idname = "import_mesh.stl" + bl_label = "Import STL" + + path = StringProperty(name="File Path", + description="File path used for importing " + "the STL file", + maxlen=1024, + default="") + filename = StringProperty(name="File Name", + description="Name of the file.") + directory = StringProperty(name="Directory", + description="Directory of the file.") + + def execute(self, context): + objName = bpy.utils.display_name(self.properties.filename) + tris, pts = stl_utils.read_stl(self.properties.path) + + blender_utils.create_and_link_mesh(objName, tris, pts) + + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.manager + wm.add_fileselect(self) + + return {'RUNNING_MODAL'} + + +class StlExporter(bpy.types.Operator): + ''' + Save STL triangle mesh data from the active object + ''' + bl_idname = "export_mesh.stl" + bl_label = "Export STL" + + path = StringProperty(name="File Path", + description="File path used for exporting " + "the active object to STL file", + maxlen=1024, + default="") + filename = StringProperty(name="File Name", + description="Name of the file.") + directory = StringProperty(name="Directory", + description="Directory of the file.") + check_existing = BoolProperty(name="Check Existing", + description="Check and warn on " + "overwriting existing files", + default=True, + options={'HIDDEN'}) + + ascii = BoolProperty(name="Ascii", + description="Save the file in ASCII file format", + default=False) + apply_modifiers = BoolProperty(name="Apply Modifiers", + description="Apply the modifiers " + "before saving", + default=True) + + def execute(self, context): + ob = context.active_object + + faces = blender_utils.faces_from_mesh(ob, + self.properties.apply_modifiers) + stl_utils.write_stl(self.properties.path, faces, self.properties.ascii) + + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.manager + wm.add_fileselect(self) + return {'RUNNING_MODAL'} + + +def menu_import(self, context): + self.layout.operator(StlImporter.bl_idname, + text="Stl (.stl)").path = "*.stl" + + +def menu_export(self, context): + default_path = bpy.data.filename.replace(".blend", ".stl") + self.layout.operator(StlExporter.bl_idname, + text="Stl (.stl)").path = default_path + + +def register(): + bpy.types.register(StlImporter) + bpy.types.register(StlExporter) + bpy.types.INFO_MT_file_import.append(menu_import) + bpy.types.INFO_MT_file_export.append(menu_export) + + +def unregister(): + bpy.types.unregister(StlImporter) + bpy.types.unregister(StlExporter) + bpy.types.INFO_MT_file_import.remove(menu_import) + bpy.types.INFO_MT_file_export.remove(menu_export) + + +if __name__ == "__main__": + register() diff --git a/io_mesh_stl/blender_utils.py b/io_mesh_stl/blender_utils.py new file mode 100644 index 00000000..1bade61d --- /dev/null +++ b/io_mesh_stl/blender_utils.py @@ -0,0 +1,50 @@ +import bpy + + +def create_and_link_mesh(name, faces, points): + ''' + Create a blender mesh and object called name from a list of + *points* and *faces* and link it in the current scene. + ''' + + mesh = bpy.data.meshes.new(name) + mesh.from_pydata(points, [], faces) + + ob = bpy.data.objects.new(name, mesh) + bpy.context.scene.objects.link(ob) + + # update mesh to allow proper display + mesh.update() + + +def faces_from_mesh(ob, apply_modifier=False, triangulate=True): + ''' + From an object, return a generator over a list of faces. + + Each faces is a list of his vertexes. Each vertex is a tuple of + his coordinate. + + apply_modifier + Apply the preview modifier to the returned liste + + triangulate + Split the quad into two triangles + ''' + + # get the modifiers + mesh = ob.create_mesh(bpy.context.scene, + True, "PREVIEW") if apply_modifier else ob.data + + def iter_face_index(): + ''' + From a list of faces, return the face triangulated if needed. + ''' + for face in mesh.faces: + if triangulate and len(face.verts) == 4: + yield face.verts[:3] + yield face.verts[2:] + [face.verts[0]] + else: + yield list(face.verts) + + return ([tuple(mesh.verts[index].co) + for index in indexes] for indexes in iter_face_index()) diff --git a/io_mesh_stl/stl_utils.py b/io_mesh_stl/stl_utils.py new file mode 100644 index 00000000..711a61d5 --- /dev/null +++ b/io_mesh_stl/stl_utils.py @@ -0,0 +1,228 @@ +''' +Import and export STL files + +Used as a blender script, it load all the stl files in the scene: + +blender -P stl_utils.py -- file1.stl file2.stl file3.stl ... +''' + +import struct +import mmap +import contextlib +import itertools + +# TODO: endien + + +@contextlib.contextmanager +def mmap_file(filename): + ''' + Context manager over the data of an mmap'ed file (Read ONLY). + + + Example: + + with mmap_file(filename) as m: + m.read() + print m[10:50] + ''' + with open(filename, 'rb') as file: + # check http://bugs.python.org/issue8046 to have mmap context + # manager fixed in python + map = mmap.mmap(file.fileno(), 0, access=mmap.ACCESS_READ) + yield map + map.close() + + +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 + + +def _binary_read(data): + # an stl binary file is + # - 80 bytes of description + # - 2 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) + + # OFFSET for the first byte of coordinate (headers + first normal bytes) + # STRIDE between each triangle (first normal + coordinates + garbage) + OFFSET, STRIDE = 84 + 12, 12 * 4 + 2 + + # read header size, ignore description + size = struct.unpack_from('<I', data, 80)[0] + unpack = struct.Struct('<9f').unpack_from + + for i in range(size): + # read the points coordinates of each triangle + pt = unpack(data, OFFSET + STRIDE * i) + yield pt[:3], pt[3:6], pt[6:] + + +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() + + while True: + # strip facet normal // or end + data.readline() + + # strip outer loup, in case of ending, break the loop + if not data.readline(): + break + + yield [tuple(map(float, data.readline().split()[1:])) + for _ in range(3)] + + # strip facet normalend and outerloop end + data.readline() + data.readline() + + +def _binary_write(filename, faces): + with open(filename, 'wb') as data: + # header + # we write padding at header begginning to avoid to + # call len(list(faces)) which may be expensive + data.write(struct.calcsize('<80sI') * b'\0') + + # 3 vertex == 9f + pack = struct.Struct('<9f').pack + # pad is to remove normal, we do use them + pad = b'\0' * struct.calcsize('<3f') + + nb = 0 + for verts in faces: + # write pad as normal + vertexes + pad as attributes + data.write(pad + pack(*itertools.chain.from_iterable(verts))) + data.write(b'\0\0') + nb += 1 + + # header, with correct value now + data.seek(0) + data.write(struct.pack('<80sI', b"Exported from blender", nb)) + + +def _ascii_write(filename, faces): + with open(filename, 'w') as data: + f.write('solid Exported from blender\n') + + for face in faces: + data.write('''facet normal 0 0 0\nouter loop\n''') + for vert in face: + data.write('vertex %f %f %f\n' % vert) + data.write('endloop\nendfacet\n') + + data.write('endsolid Exported from blender\n') + + +def write_stl(filename, faces, ascii=False): + ''' + Write a stl file from faces, + + filename + output filename + + 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)(filename, faces) + + +def read_stl(filename): + ''' + 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, points). + + triangles + A list of triangles, each triangle as a tuple of 3 index of + point in *points*. + + points + An indexed list of points, each point is a tuple of 3 float + (xyz). + + Example of use: + + >>> tris, pts = read_stl(filename, lambda x:) + >>> pts = list(pts) + >>> + >>> # print the coordinate of the triangle n + >>> print([pts[i] for i in tris[n]]) + ''' + + tris, pts = [], ListDict() + + with mmap_file(filename) as data: + # check for ascii or binary + gen = _ascii_read if data.read(5) == b'solid' else _binary_read + + for pt in gen(data): + # Add the triangle and the point. + # If the point is allready 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]) + + return tris, pts.list + + +if __name__ == '__main__': + import sys + import bpy + from io_mesh_stl import blender_utils + + filenames = sys.argv[sys.argv.index('--') + 1:] + + for filename in filenames: + objName = bpy.utils.display_name(filename) + tris, pts = read_stl(filename) + + blender_utils.create_and_link_mesh(objName, tris, pts) |