1# ##### BEGIN GPL LICENSE BLOCK #####
2#
3#  This program is free software; you can redistribute it and/or
4#  modify it under the terms of the GNU General Public License
5#  as published by the Free Software Foundation; either version 2
6#  of the License, or (at your option) any later version.
7#
8#  This program is distributed in the hope that it will be useful,
9#  but WITHOUT ANY WARRANTY; without even the implied warranty of
10#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11#  GNU General Public License for more details.
12#
13#  You should have received a copy of the GNU General Public License
14#  along with this program; if not, write to the Free Software Foundation,
15#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16#
17# ##### END GPL LICENSE BLOCK #####
18
19# <pep8-80 compliant>
20
21bl_info = {
22    "name": "STL format",
23    "author": "Guillaume Bouchard (Guillaum)",
24    "version": (1, 1, 3),
25    "blender": (2, 81, 6),
26    "location": "File > Import-Export",
27    "description": "Import-Export STL files",
28    "doc_url": "{BLENDER_MANUAL_URL}/addons/import_export/mesh_stl.html",
29    "support": 'OFFICIAL',
30    "category": "Import-Export",
31}
32
33
34# @todo write the wiki page
35
36"""
37Import-Export STL files (binary or ascii)
38
39- Import automatically remove the doubles.
40- Export can export with/without modifiers applied
41
42Issues:
43
44Import:
45    - Does not handle endien
46"""
47
48if "bpy" in locals():
49    import importlib
50    if "stl_utils" in locals():
51        importlib.reload(stl_utils)
52    if "blender_utils" in locals():
53        importlib.reload(blender_utils)
54
55import bpy
56from bpy.props import (
57    StringProperty,
58    BoolProperty,
59    CollectionProperty,
60    EnumProperty,
61    FloatProperty,
62)
63from bpy_extras.io_utils import (
64    ImportHelper,
65    ExportHelper,
66    orientation_helper,
67    axis_conversion,
68)
69from bpy.types import (
70    Operator,
71    OperatorFileListElement,
72)
73
74
75@orientation_helper(axis_forward='Y', axis_up='Z')
76class ImportSTL(Operator, ImportHelper):
77    bl_idname = "import_mesh.stl"
78    bl_label = "Import STL"
79    bl_description = "Load STL triangle mesh data"
80    bl_options = {'UNDO'}
81
82    filename_ext = ".stl"
83
84    filter_glob: StringProperty(
85        default="*.stl",
86        options={'HIDDEN'},
87    )
88    files: CollectionProperty(
89        name="File Path",
90        type=OperatorFileListElement,
91    )
92    directory: StringProperty(
93        subtype='DIR_PATH',
94    )
95    global_scale: FloatProperty(
96        name="Scale",
97        soft_min=0.001, soft_max=1000.0,
98        min=1e-6, max=1e6,
99        default=1.0,
100    )
101    use_scene_unit: BoolProperty(
102        name="Scene Unit",
103        description="Apply current scene's unit (as defined by unit scale) to imported data",
104        default=False,
105    )
106    use_facet_normal: BoolProperty(
107        name="Facet Normals",
108        description="Use (import) facet normals (note that this will still give flat shading)",
109        default=False,
110    )
111
112    def execute(self, context):
113        import os
114        from mathutils import Matrix
115        from . import stl_utils
116        from . import blender_utils
117
118        paths = [os.path.join(self.directory, name.name) for name in self.files]
119
120        scene = context.scene
121
122        # Take into account scene's unit scale, so that 1 inch in Blender gives 1 inch elsewhere! See T42000.
123        global_scale = self.global_scale
124        if scene.unit_settings.system != 'NONE' and self.use_scene_unit:
125            global_scale /= scene.unit_settings.scale_length
126
127        global_matrix = axis_conversion(
128            from_forward=self.axis_forward,
129            from_up=self.axis_up,
130        ).to_4x4() @ Matrix.Scale(global_scale, 4)
131
132        if not paths:
133            paths.append(self.filepath)
134
135        if bpy.ops.object.mode_set.poll():
136            bpy.ops.object.mode_set(mode='OBJECT')
137
138        if bpy.ops.object.select_all.poll():
139            bpy.ops.object.select_all(action='DESELECT')
140
141        for path in paths:
142            objName = bpy.path.display_name(os.path.basename(path))
143            tris, tri_nors, pts = stl_utils.read_stl(path)
144            tri_nors = tri_nors if self.use_facet_normal else None
145            blender_utils.create_and_link_mesh(objName, tris, tri_nors, pts, global_matrix)
146
147        return {'FINISHED'}
148
149    def draw(self, context):
150        pass
151
152
153class STL_PT_import_transform(bpy.types.Panel):
154    bl_space_type = 'FILE_BROWSER'
155    bl_region_type = 'TOOL_PROPS'
156    bl_label = "Transform"
157    bl_parent_id = "FILE_PT_operator"
158
159    @classmethod
160    def poll(cls, context):
161        sfile = context.space_data
162        operator = sfile.active_operator
163
164        return operator.bl_idname == "IMPORT_MESH_OT_stl"
165
166    def draw(self, context):
167        layout = self.layout
168        layout.use_property_split = True
169        layout.use_property_decorate = False  # No animation.
170
171        sfile = context.space_data
172        operator = sfile.active_operator
173
174        layout.prop(operator, "global_scale")
175        layout.prop(operator, "use_scene_unit")
176
177        layout.prop(operator, "axis_forward")
178        layout.prop(operator, "axis_up")
179
180
181class STL_PT_import_geometry(bpy.types.Panel):
182    bl_space_type = 'FILE_BROWSER'
183    bl_region_type = 'TOOL_PROPS'
184    bl_label = "Geometry"
185    bl_parent_id = "FILE_PT_operator"
186
187    @classmethod
188    def poll(cls, context):
189        sfile = context.space_data
190        operator = sfile.active_operator
191
192        return operator.bl_idname == "IMPORT_MESH_OT_stl"
193
194    def draw(self, context):
195        layout = self.layout
196        layout.use_property_split = True
197        layout.use_property_decorate = False  # No animation.
198
199        sfile = context.space_data
200        operator = sfile.active_operator
201
202        layout.prop(operator, "use_facet_normal")
203
204
205@orientation_helper(axis_forward='Y', axis_up='Z')
206class ExportSTL(Operator, ExportHelper):
207    bl_idname = "export_mesh.stl"
208    bl_label = "Export STL"
209    bl_description = """Save STL triangle mesh data"""
210
211    filename_ext = ".stl"
212    filter_glob: StringProperty(default="*.stl", options={'HIDDEN'})
213
214    use_selection: BoolProperty(
215        name="Selection Only",
216        description="Export selected objects only",
217        default=False,
218    )
219    global_scale: FloatProperty(
220        name="Scale",
221        min=0.01, max=1000.0,
222        default=1.0,
223    )
224    use_scene_unit: BoolProperty(
225        name="Scene Unit",
226        description="Apply current scene's unit (as defined by unit scale) to exported data",
227        default=False,
228    )
229    ascii: BoolProperty(
230        name="Ascii",
231        description="Save the file in ASCII file format",
232        default=False,
233    )
234    use_mesh_modifiers: BoolProperty(
235        name="Apply Modifiers",
236        description="Apply the modifiers before saving",
237        default=True,
238    )
239    batch_mode: EnumProperty(
240        name="Batch Mode",
241        items=(
242            ('OFF', "Off", "All data in one file"),
243            ('OBJECT', "Object", "Each object as a file"),
244        ),
245    )
246
247    @property
248    def check_extension(self):
249        return self.batch_mode == 'OFF'
250
251    def execute(self, context):
252        import os
253        import itertools
254        from mathutils import Matrix
255        from . import stl_utils
256        from . import blender_utils
257
258        keywords = self.as_keywords(
259            ignore=(
260                "axis_forward",
261                "axis_up",
262                "use_selection",
263                "global_scale",
264                "check_existing",
265                "filter_glob",
266                "use_scene_unit",
267                "use_mesh_modifiers",
268                "batch_mode"
269            ),
270        )
271
272        scene = context.scene
273        if self.use_selection:
274            data_seq = context.selected_objects
275        else:
276            data_seq = scene.objects
277
278        # Take into account scene's unit scale, so that 1 inch in Blender gives 1 inch elsewhere! See T42000.
279        global_scale = self.global_scale
280        if scene.unit_settings.system != 'NONE' and self.use_scene_unit:
281            global_scale *= scene.unit_settings.scale_length
282
283        global_matrix = axis_conversion(
284            to_forward=self.axis_forward,
285            to_up=self.axis_up,
286        ).to_4x4() @ Matrix.Scale(global_scale, 4)
287
288        if self.batch_mode == 'OFF':
289            faces = itertools.chain.from_iterable(
290                    blender_utils.faces_from_mesh(ob, global_matrix, self.use_mesh_modifiers)
291                    for ob in data_seq)
292
293            stl_utils.write_stl(faces=faces, **keywords)
294        elif self.batch_mode == 'OBJECT':
295            prefix = os.path.splitext(self.filepath)[0]
296            keywords_temp = keywords.copy()
297            for ob in data_seq:
298                faces = blender_utils.faces_from_mesh(ob, global_matrix, self.use_mesh_modifiers)
299                keywords_temp["filepath"] = prefix + bpy.path.clean_name(ob.name) + ".stl"
300                stl_utils.write_stl(faces=faces, **keywords_temp)
301
302        return {'FINISHED'}
303
304    def draw(self, context):
305        pass
306
307
308class STL_PT_export_main(bpy.types.Panel):
309    bl_space_type = 'FILE_BROWSER'
310    bl_region_type = 'TOOL_PROPS'
311    bl_label = ""
312    bl_parent_id = "FILE_PT_operator"
313    bl_options = {'HIDE_HEADER'}
314
315    @classmethod
316    def poll(cls, context):
317        sfile = context.space_data
318        operator = sfile.active_operator
319
320        return operator.bl_idname == "EXPORT_MESH_OT_stl"
321
322    def draw(self, context):
323        layout = self.layout
324        layout.use_property_split = True
325        layout.use_property_decorate = False  # No animation.
326
327        sfile = context.space_data
328        operator = sfile.active_operator
329
330        layout.prop(operator, "ascii")
331        layout.prop(operator, "batch_mode")
332
333
334class STL_PT_export_include(bpy.types.Panel):
335    bl_space_type = 'FILE_BROWSER'
336    bl_region_type = 'TOOL_PROPS'
337    bl_label = "Include"
338    bl_parent_id = "FILE_PT_operator"
339
340    @classmethod
341    def poll(cls, context):
342        sfile = context.space_data
343        operator = sfile.active_operator
344
345        return operator.bl_idname == "EXPORT_MESH_OT_stl"
346
347    def draw(self, context):
348        layout = self.layout
349        layout.use_property_split = True
350        layout.use_property_decorate = False  # No animation.
351
352        sfile = context.space_data
353        operator = sfile.active_operator
354
355        layout.prop(operator, "use_selection")
356
357
358class STL_PT_export_transform(bpy.types.Panel):
359    bl_space_type = 'FILE_BROWSER'
360    bl_region_type = 'TOOL_PROPS'
361    bl_label = "Transform"
362    bl_parent_id = "FILE_PT_operator"
363
364    @classmethod
365    def poll(cls, context):
366        sfile = context.space_data
367        operator = sfile.active_operator
368
369        return operator.bl_idname == "EXPORT_MESH_OT_stl"
370
371    def draw(self, context):
372        layout = self.layout
373        layout.use_property_split = True
374        layout.use_property_decorate = False  # No animation.
375
376        sfile = context.space_data
377        operator = sfile.active_operator
378
379        layout.prop(operator, "global_scale")
380        layout.prop(operator, "use_scene_unit")
381
382        layout.prop(operator, "axis_forward")
383        layout.prop(operator, "axis_up")
384
385
386class STL_PT_export_geometry(bpy.types.Panel):
387    bl_space_type = 'FILE_BROWSER'
388    bl_region_type = 'TOOL_PROPS'
389    bl_label = "Geometry"
390    bl_parent_id = "FILE_PT_operator"
391
392    @classmethod
393    def poll(cls, context):
394        sfile = context.space_data
395        operator = sfile.active_operator
396
397        return operator.bl_idname == "EXPORT_MESH_OT_stl"
398
399    def draw(self, context):
400        layout = self.layout
401        layout.use_property_split = True
402        layout.use_property_decorate = False  # No animation.
403
404        sfile = context.space_data
405        operator = sfile.active_operator
406
407        layout.prop(operator, "use_mesh_modifiers")
408
409
410def menu_import(self, context):
411    self.layout.operator(ImportSTL.bl_idname, text="Stl (.stl)")
412
413
414def menu_export(self, context):
415    self.layout.operator(ExportSTL.bl_idname, text="Stl (.stl)")
416
417
418classes = (
419    ImportSTL,
420    STL_PT_import_transform,
421    STL_PT_import_geometry,
422    ExportSTL,
423    STL_PT_export_main,
424    STL_PT_export_include,
425    STL_PT_export_transform,
426    STL_PT_export_geometry,
427)
428
429
430def register():
431    for cls in classes:
432        bpy.utils.register_class(cls)
433
434    bpy.types.TOPBAR_MT_file_import.append(menu_import)
435    bpy.types.TOPBAR_MT_file_export.append(menu_export)
436
437
438def unregister():
439    for cls in classes:
440        bpy.utils.unregister_class(cls)
441
442    bpy.types.TOPBAR_MT_file_import.remove(menu_import)
443    bpy.types.TOPBAR_MT_file_export.remove(menu_export)
444
445
446if __name__ == "__main__":
447    register()
448