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