├── README.md └── io_export_selected.py /README.md: -------------------------------------------------------------------------------- 1 | Export Selected 2 | =============== 3 | 4 | A Blender Addon that provides a unified way to export selected objects to 5 | the registered export formats (or even to a .blend file) 6 | 7 | Wiki page 8 | --------- 9 | 10 | http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Import-Export/Export_Selected 11 | 12 | Thanks 13 | ------ 14 | 15 | - moth3r / Ivan - For the original idea of exporting to Blend file. 16 | - rking / Ryan Joseph King - For the original idea and help with the 17 | repository setup. 18 | -------------------------------------------------------------------------------- /io_export_selected.py: -------------------------------------------------------------------------------- 1 | # ***** BEGIN GPL LICENSE BLOCK ***** 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (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, see . 15 | # 16 | # ***** END GPL LICENSE BLOCK ***** 17 | 18 | bl_info = { 19 | "name": "Export Selected", 20 | "author": "dairin0d, rking, moth3r", 21 | "version": (2, 2, 2), 22 | "blender": (2, 7, 0), 23 | "location": "File > Export > Selected", 24 | "description": "Export selected objects to a chosen format", 25 | "warning": "", 26 | "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Import-Export/Export_Selected", 27 | "tracker_url": "https://github.com/dairin0d/export-selected/issues", 28 | "category": "Import-Export"} 29 | #============================================================================# 30 | 31 | # TODO: 32 | # * implement dynamic exporter properties differently (current implementation cannot support simultaneously opened export UIs if their export formats are different) 33 | # * batch import (Blender allows selecting multiple files, but only one of them is actually imported) 34 | 35 | import bpy 36 | 37 | from bpy_extras.io_utils import ExportHelper, ImportHelper 38 | 39 | from mathutils import Vector, Matrix, Quaternion, Euler 40 | 41 | from collections import namedtuple 42 | 43 | import os 44 | import json 45 | import re 46 | import hashlib 47 | 48 | def bpy_path_normslash(path): 49 | return path.replace(os.path.sep, "/") 50 | 51 | def bpy_path_join(*paths): 52 | # use os.path.join logic (it's not that simple) 53 | return bpy_path_normslash(os.path.join(*paths)) 54 | 55 | def bpy_path_splitext(path): 56 | path = bpy_path_normslash(path) 57 | i_split = path.rfind(".") 58 | if i_split < 0: return (path, "") 59 | return (path[:i_split], path[i_split:]) 60 | 61 | # For some reason, when path contains "//", os.path.split ignores single slashes 62 | # When path ends with slash, return dir without slash, except when it's / or // 63 | def bpy_path_split(path): 64 | path = bpy_path_normslash(path) 65 | i_split = path.rfind("/") + 1 66 | dir_part = path[:i_split] 67 | file_part = path[i_split:] 68 | dir_part_strip = dir_part.rstrip("/") 69 | if dir_part_strip: dir_part = dir_part[:len(dir_part_strip)] 70 | return (dir_part, file_part) 71 | 72 | def bpy_path_dirname(path): 73 | return bpy_path_split(path)[0] 74 | 75 | def bpy_path_basename(path): 76 | return bpy_path_split(path)[1] 77 | 78 | operator_presets_dir = bpy_path_join(bpy.utils.resource_path('USER'), "scripts", "presets", "operator") 79 | 80 | object_types = ['MESH', 'CURVE', 'SURFACE', 'META', 'FONT', 'ARMATURE', 'LATTICE', 'EMPTY', 'CAMERA', 'LAMP', 'SPEAKER'] 81 | 82 | bpy_props = { 83 | bpy.props.BoolProperty, 84 | bpy.props.BoolVectorProperty, 85 | bpy.props.IntProperty, 86 | bpy.props.IntVectorProperty, 87 | bpy.props.FloatProperty, 88 | bpy.props.FloatVectorProperty, 89 | bpy.props.StringProperty, 90 | bpy.props.EnumProperty, 91 | bpy.props.PointerProperty, 92 | bpy.props.CollectionProperty, 93 | } 94 | 95 | def is_bpy_prop(value): 96 | return (isinstance(value, tuple) and (len(value) == 2) and (value[0] in bpy_props) and isinstance(value[1], dict)) 97 | 98 | def iter_public_bpy_props(cls, exclude_hidden=False): 99 | for key in dir(cls): 100 | if key.startswith("_"): continue 101 | value = getattr(cls, key) 102 | if not is_bpy_prop(value): continue 103 | if exclude_hidden: 104 | options = value[1].get("options", "") 105 | if 'HIDDEN' in options: continue 106 | yield (key, value) 107 | 108 | def get_op(idname): 109 | category_name, op_name = idname.split(".") 110 | category = getattr(bpy.ops, category_name) 111 | return getattr(category, op_name) 112 | 113 | def layers_intersect(a, b, name_a="layers", name_b=None): 114 | return any(l0 and l1 for l0, l1 in zip(getattr(a, name_a), getattr(b, name_b or name_a))) 115 | 116 | def obj_root(obj): 117 | while obj.parent: 118 | obj = obj.parent 119 | return obj 120 | 121 | def obj_parents(obj): 122 | while obj.parent: 123 | yield obj.parent 124 | obj = obj.parent 125 | 126 | def belongs_to_group(obj, group, consider_dupli=False): 127 | if not obj: return None 128 | # Object is either IN some group or INSTANTIATES that group, never both 129 | if obj.dupli_group == group: 130 | return ('DUPLI' if consider_dupli else None) 131 | elif obj.name in group.objects: 132 | return 'PART' 133 | return None 134 | 135 | # FRAMES copies the object itself, but not its children 136 | # VERTS and FACES copy the children 137 | # GROUP copies the group contents 138 | 139 | def get_dupli_roots(obj, scene=None, settings='VIEWPORT'): 140 | if (not obj) or (obj.dupli_type == 'NONE'): return None 141 | if not scene: scene = bpy.context.scene 142 | 143 | filter = None 144 | if obj.dupli_type in ('VERTS', 'FACES'): 145 | filter = set(obj.children) 146 | elif (obj.dupli_type == 'GROUP') and obj.dupli_group: 147 | filter = set(obj.dupli_group.objects) 148 | 149 | roots = [] 150 | if obj.dupli_list: obj.dupli_list_clear() 151 | obj.dupli_list_create(scene, settings) 152 | for dupli in obj.dupli_list: 153 | if (not filter) or (dupli.object in filter): 154 | roots.append((dupli.object, Matrix(dupli.matrix))) 155 | obj.dupli_list_clear() 156 | 157 | return roots 158 | 159 | def instantiate_duplis(obj, scene=None, settings='VIEWPORT', depth=-1): 160 | if (not obj) or (obj.dupli_type == 'NONE'): return 161 | if not scene: scene = bpy.context.scene 162 | 163 | if depth == 0: return 164 | if depth > 0: depth -= 1 165 | 166 | roots = get_dupli_roots(obj, scene, settings) 167 | 168 | dupli_type = obj.dupli_type 169 | # Prevent recursive copying in FRAMES dupli mode 170 | obj.dupli_type = 'NONE' 171 | 172 | dst_info = [] 173 | src_dst = {} 174 | for src_obj, matrix in roots: 175 | dst_obj = src_obj.copy() 176 | dst_obj.constraints.clear() 177 | scene.objects.link(dst_obj) 178 | if dupli_type == 'FRAMES': 179 | dst_obj.animation_data_clear() 180 | dst_info.append((dst_obj, src_obj, matrix)) 181 | src_dst[src_obj] = dst_obj 182 | 183 | scene.update() # <-- important 184 | 185 | for dst_obj, src_obj, matrix in dst_info: 186 | dst_parent = src_dst.get(src_obj.parent) 187 | if dst_parent: 188 | # parent_type, parent_bone, parent_vertices 189 | # should be copied automatically 190 | dst_obj.parent = dst_parent 191 | else: 192 | dst_obj.parent_type = 'OBJECT' 193 | dst_obj.parent = obj 194 | 195 | for dst_obj, src_obj, matrix in dst_info: 196 | dst_obj.matrix_world = matrix 197 | 198 | for dst_obj, src_obj, matrix in dst_info: 199 | instantiate_duplis(dst_obj, scene, settings, depth) 200 | 201 | class PrimitiveLock(object): 202 | "Primary use of such lock is to prevent infinite recursion" 203 | def __init__(self): 204 | self.count = 0 205 | def __bool__(self): 206 | return bool(self.count) 207 | def __enter__(self): 208 | self.count += 1 209 | def __exit__(self, exc_type, exc_value, exc_traceback): 210 | self.count -= 1 211 | 212 | class ToggleObjectMode: 213 | def __init__(self, mode='OBJECT', undo=False): 214 | if not isinstance(mode, str): 215 | mode = ('OBJECT' if mode else None) 216 | 217 | obj = bpy.context.object 218 | self.mode = (mode if obj and (obj.mode != mode) else None) 219 | self.undo = undo 220 | 221 | def __enter__(self): 222 | if self.mode: 223 | edit_preferences = bpy.context.user_preferences.edit 224 | 225 | self.global_undo = edit_preferences.use_global_undo 226 | # if self.mode == True, bpy.context.object exists 227 | self.prev_mode = bpy.context.object.mode 228 | 229 | if self.prev_mode != self.mode: 230 | if self.undo is not None: 231 | edit_preferences.use_global_undo = self.undo 232 | bpy.ops.object.mode_set(mode=self.mode) 233 | 234 | return self 235 | 236 | def __exit__(self, type, value, traceback): 237 | if self.mode: 238 | edit_preferences = bpy.context.user_preferences.edit 239 | 240 | if self.prev_mode != self.mode: 241 | bpy.ops.object.mode_set(mode=self.prev_mode) 242 | edit_preferences.use_global_undo = self.global_undo 243 | 244 | #============================================================================# 245 | 246 | # Adapted from https://gist.github.com/regularcoder/8254723 247 | def fletcher(data, n): # n should be 16, 32 or 64 248 | nbytes = min(max(n // 16, 1), 4) 249 | mod = 2 ** (8 * nbytes) - 1 250 | sum1 = sum2 = 0 251 | for i in range(0, len(data), nbytes): 252 | block = int.from_bytes(data[i:i + nbytes], 'little') 253 | sum1 = (sum1 + block) % mod 254 | sum2 = (sum2 + sum1) % mod 255 | return sum1 + (sum2 * (mod+1)) 256 | 257 | def hashnames(): 258 | hashnames_codes = [chr(o) for o in range(ord("0"), ord("9")+1)] 259 | hashnames_codes += [chr(o) for o in range(ord("A"), ord("Z")+1)] 260 | n = len(hashnames_codes) 261 | def _hashnames(names): 262 | binary_data = "\0".join(sorted(names)).encode() 263 | hash_value = fletcher(binary_data, 32) 264 | result = [] 265 | while True: 266 | k = hash_value % n 267 | result.append(hashnames_codes[k]) 268 | hash_value = (hash_value - k) // n 269 | if hash_value == 0: break 270 | return "".join(result) 271 | return _hashnames 272 | hashnames = hashnames() 273 | 274 | def replace_extension(path, ext): 275 | name = bpy_path_basename(path) 276 | if name and not name.lower().endswith(ext.lower()): 277 | path = bpy_path_splitext(path)[0] + ext 278 | return path 279 | 280 | forbidden_chars = "\x00-\x1f/" # on all OSes 281 | forbidden_chars += "<>:\"|?*\\\\" # on Windows/FAT/NTFS 282 | forbidden_chars = "["+forbidden_chars+"]" 283 | def clean_filename(filename, sub="-"): 284 | return re.sub(forbidden_chars, sub, filename) 285 | 286 | #============================================================================# 287 | 288 | def iter_exporters(): 289 | for category_name in dir(bpy.ops): 290 | if "export" not in category_name: continue 291 | op_category = getattr(bpy.ops, category_name) 292 | for name in dir(op_category): 293 | idname = category_name + "." + name 294 | if idname == ExportSelected.bl_idname: continue 295 | if "export" not in idname: continue 296 | yield (idname, getattr(op_category, name)) 297 | 298 | def get_instance_type_or_emulator(obj): 299 | if hasattr(obj, "get_instance"): return type(obj.get_instance()) 300 | rna_type = obj.get_rna_type() 301 | rna_props = rna_type.properties 302 | # namedtuple fields can't start with underscores, but so do rna props 303 | return namedtuple(rna_type.identifier, rna_props.keys())(*rna_props.values()) # For Blender 2.79.6 304 | 305 | def get_rna_type(obj): 306 | if hasattr(obj, "rna_type"): return obj.rna_type 307 | if hasattr(obj, "get_rna"): return obj.get_rna().rna_type 308 | return obj.get_rna_type() # For Blender 2.79.6 309 | 310 | def get_filter_glob(op, default_filter): 311 | if hasattr(op, "get_rna"): 312 | rna = op.get_rna() 313 | return getattr(rna, "filter_glob", default_filter) 314 | # There is no get_rna() in Blender 2.79.6 315 | return op.get_rna_type().properties.get("filter_glob", default_filter) 316 | 317 | def iter_exporter_info(): 318 | # Special case: unconventional "exporter" 319 | yield ('BLEND', "Blend", ".blend", "*.blend") 320 | 321 | # Special case: unconventional operator name, ext/glob aren't exposed 322 | yield ('wm.collada_export', "Collada", ".dae", "*.dae") 323 | 324 | # Special case: unconventional operator name, ext/glob aren't exposed 325 | yield ('wm.alembic_export', "Alembic", ".abc", "*.abc") 326 | 327 | for idname, op in iter_exporters(): 328 | op_class = get_instance_type_or_emulator(op) 329 | if not hasattr(op_class, "filepath"): continue # e.g. sketchfab 330 | name = get_rna_type(op).name 331 | if name.lower().startswith("export "): name = name[len("export "):] 332 | filename_ext = getattr(op_class, "filename_ext", "") 333 | if not isinstance(filename_ext, str): # can be a bpy prop 334 | filename_ext = filename_ext[1].get("default", "") 335 | if not filename_ext: filename_ext = "."+idname.split(".")[-1] 336 | filter_glob = get_filter_glob(op, "*"+filename_ext) 337 | yield (idname, name, filename_ext, filter_glob) 338 | 339 | def get_exporter_name(idname): 340 | if idname == 'BLEND': return "Blend" 341 | op = get_op(idname) 342 | name = get_rna_type(op).name 343 | if name.lower().startswith("export "): name = name[len("export "):] 344 | return name 345 | 346 | def get_exporter_class(idname): 347 | if idname == 'BLEND': 348 | return BlendExportEmulator 349 | elif idname == 'wm.collada_export': 350 | return ColladaExportEmulator 351 | elif idname == 'wm.alembic_export': 352 | return AlembicExportEmulator 353 | else: 354 | op = get_op(idname) 355 | return get_instance_type_or_emulator(op) 356 | 357 | class BlendExportEmulator: 358 | # Special case: Blend 359 | compress = bpy.props.BoolProperty(name="Compress", description="Write compressed .blend file", default=False) 360 | relative_remap = bpy.props.BoolProperty(name="Remap Relative", description="Remap relative paths when saving in a different directory", default=True) 361 | 362 | class ColladaExportEmulator: 363 | # Special case: Collada (built-in) -- has no explicitly defined Python properties 364 | apply_modifiers = bpy.props.BoolProperty(name="Apply Modifiers", description="Apply modifiers to exported mesh (non destructive)", default=False) 365 | export_mesh_type_selection = bpy.props.EnumProperty(name="Type of modifiers", description="Modifier resolution for export", default='view', items=[('render', "Render", "Apply modifier's render settings"), ('view', "View", "Apply modifier's view settings")]) 366 | selected = bpy.props.BoolProperty(name="Selection Only", description="Export only selected elements", default=False) 367 | include_children = bpy.props.BoolProperty(name="Include Children", description="Export all children of selected objects (even if not selected)", default=False) 368 | include_armatures = bpy.props.BoolProperty(name="Include Armatures", description="Export related armatures (even if not selected)", default=False) 369 | include_shapekeys = bpy.props.BoolProperty(name="Include Shape Keys", description="Export all Shape Keys from Mesh Objects", default=True) 370 | active_uv_only = bpy.props.BoolProperty(name="Only Active UV layer", description="Export textures assigned to the object UV maps", default=False) 371 | if bpy.app.version < (2, 79, 0): 372 | include_uv_textures = bpy.props.BoolProperty(name="Include UV Textures", description="Export textures assigned to the object UV maps", default=False) 373 | include_material_textures = bpy.props.BoolProperty(name="Include Material Textures", description="Export textures assigned to the object Materials", default=False) 374 | if bpy.app.version >= (2, 79, 0): 375 | export_texture_type_selection = bpy.props.EnumProperty(name="Texture Type", description="Type for exported Textures (UV or MAT)", default='mat', items=[('mat', "Materials", "Export Materials"), ('uv', "UV Textures", "Export UV Textures (Face textures) as materials")]) 376 | use_texture_copies = bpy.props.BoolProperty(name="Copy Textures", description="Copy textures to the same folder where .dae file is exported", default=True) 377 | deform_bones_only = bpy.props.BoolProperty(name="Deform Bones only", description="Only export deforming bones with armatures", default=False) 378 | open_sim = bpy.props.BoolProperty(name="Export for OpenSim", description="Compatibility mode for OpenSim and compatible online worlds", default=False) 379 | triangulate = bpy.props.BoolProperty(name="Triangulate", description="Export Polygons (Quads & NGons) as Triangles", default=True) 380 | use_object_instantiation = bpy.props.BoolProperty(name="Use Object Instances", description="Instantiate multiple Objects from same Data", default=True) 381 | export_transformation_type_selection = bpy.props.EnumProperty(name="Transformation Type", description="Transformation type for translation, scale and rotation", default='matrix', items=[('both', "Both", "Use AND , , to specify transformations"), ('transrotloc', "TransLocRot", "Use , , to specify transformations"), ('matrix', "Matrix", "Use to specify transformations")]) 382 | sort_by_name = bpy.props.BoolProperty(name="Sort by Object name", description="Sort exported data by Object name", default=False) 383 | if bpy.app.version >= (2, 79, 0): 384 | keep_bind_info = bpy.props.BoolProperty(name="Keep Bind Info", description="Store Bindpose information in custom bone properties for latter use during Collada export", default=False) 385 | limit_precision = bpy.props.BoolProperty(name="Limit Precision", description="Reduce the precision of the exported data to 6 digits", default=False) 386 | 387 | def draw(self, context): 388 | layout = self.layout 389 | 390 | box = layout.box() 391 | box.label(text="Export Data Options", icon='MESH_DATA') 392 | row = box.split(0.6) 393 | row.prop(self, "apply_modifiers") 394 | row.prop(self, "export_mesh_type_selection", text="") 395 | box.prop(self, "selected") 396 | box.prop(self, "include_children") 397 | box.prop(self, "include_armatures") 398 | box.prop(self, "include_shapekeys") 399 | 400 | box = layout.box() 401 | box.label(text="Texture Options", icon='TEXTURE') 402 | box.prop(self, "active_uv_only") 403 | if bpy.app.version < (2, 79, 0): 404 | box.prop(self, "include_uv_textures") 405 | box.prop(self, "include_material_textures") 406 | if bpy.app.version >= (2, 79, 0): 407 | box.prop(self, "export_texture_type_selection", text="") 408 | box.prop(self, "use_texture_copies", text="Copy") 409 | 410 | box = layout.box() 411 | box.label(text="Armature Options", icon='ARMATURE_DATA') 412 | box.prop(self, "deform_bones_only") 413 | box.prop(self, "open_sim") 414 | 415 | box = layout.box() 416 | box.label(text="Collada Options", icon='MODIFIER') 417 | box.prop(self, "triangulate") 418 | box.prop(self, "use_object_instantiation") 419 | row = box.split(0.6) 420 | row.label(text="Transformation Type") 421 | row.prop(self, "export_transformation_type_selection", text="") 422 | box.prop(self, "sort_by_name") 423 | if bpy.app.version >= (2, 79, 0): 424 | box.prop(self, "keep_bind_info") 425 | box.prop(self, "limit_precision") 426 | 427 | class AlembicExportEmulator: 428 | # Special case: Alembic (built-in) -- has no explicitly defined Python properties 429 | global_scale = bpy.props.FloatProperty(name="Scale", description="Value by which to enlarge or shrink the objects with respect to the world's origin", default=1.0, min=0.0001, max=1000.0, step=1, precision=3) 430 | start = bpy.props.IntProperty(name="Start Frame", description="Start Frame", default=1) 431 | end = bpy.props.IntProperty(name="End Frame", description="End Frame", default=1) 432 | xsamples = bpy.props.IntProperty(name="Transform Samples", description="Number of times per frame transformations are sampled", default=1, min=1, max=128) 433 | gsamples = bpy.props.IntProperty(name="Geometry Samples", description="Number of times per frame object data are sampled", default=1, min=1, max=128) 434 | sh_open = bpy.props.FloatProperty(name="Shutter Open", description="Time at which the shutter is open", default=0.0, min=-1, max=1, step=1, precision=3) 435 | sh_close = bpy.props.FloatProperty(name="Shutter Close", description="Time at which the shutter is closed", default=1.0, min=-1, max=1, step=1, precision=3) 436 | selected = bpy.props.BoolProperty(name="Selected Objects Only", description="Export only selected objects", default=False) 437 | renderable_only = bpy.props.BoolProperty(name="Renderable Objects Only", description="Export only objects marked renderable in the outliner", default=True) 438 | visible_layers_only = bpy.props.BoolProperty(name="Visible Layers Only", description="Export only objects in visible layers", default=False) 439 | flatten = bpy.props.BoolProperty(name="Flatten Hierarchy", description="Do not preserve objects' parent/children relationship", default=False) 440 | uvs = bpy.props.BoolProperty(name="UVs", description="Export UVs", default=True) 441 | packuv = bpy.props.BoolProperty(name="Pack UV Islands", description="Export UVs with packed island", default=True) 442 | normals = bpy.props.BoolProperty(name="Normals", description="Export normals", default=True) 443 | vcolors = bpy.props.BoolProperty(name="Vertex Colors", description="Export vertex colors", default=False) 444 | face_sets = bpy.props.BoolProperty(name="Face Sets", description="Export per face shading group assignments", default=False) 445 | subdiv_schema = bpy.props.BoolProperty(name="Use Subdivision Schema", description="Export meshes using Alembic's subdivision schema", default=False) 446 | apply_subdiv = bpy.props.BoolProperty(name="Apply Subsurf", description="Export subdivision surfaces as meshes", default=False) 447 | if bpy.app.version >= (2, 79, 0): 448 | triangulate = bpy.props.BoolProperty(name="Triangulate", description="Export Polygons (Quads & NGons) as Triangles", default=False) 449 | quad_method = bpy.props.EnumProperty(name="Quad Method", description="Method for splitting the quads into triangles", default='SHORTEST_DIAGONAL', items=[('BEAUTY', "Beauty", "Split the quads in nice triangles, slower method."), ('FIXED', "Fixed", "Split the quads on the first and third vertices."), ('FIXED_ALTERNATE', "Fixed Alternate", "Split the quads on the 2nd and 4th vertices."), ('SHORTEST_DIAGONAL', "Shortest Diagonal", "Split the quads based on the distance between the vertices.")]) 450 | ngon_method = bpy.props.EnumProperty(name="Polygon Method", description="Method for splitting the polygons into triangles", default='SHORTEST_DIAGONAL', items=[('BEAUTY', "Beauty", "Split the quads in nice triangles, slower method."), ('FIXED', "Fixed", "Split the quads on the first and third vertices."), ('FIXED_ALTERNATE', "Fixed Alternate", "Split the quads on the 2nd and 4th vertices."), ('SHORTEST_DIAGONAL', "Shortest Diagonal", "Split the quads based on the distance between the vertices.")]) 451 | export_hair = bpy.props.BoolProperty(name="Export Hair", description="Exports hair particle systems as animated curves", default=True) 452 | export_particles = bpy.props.BoolProperty(name="Export Particles", description="Exports non-hair particle systems", default=True) 453 | 454 | def draw(self, context): 455 | layout = self.layout 456 | 457 | box = layout.box() 458 | box.label(text="Manual Transform:") 459 | box.prop(self, "global_scale") 460 | 461 | box = layout.box() 462 | box.label(text="Scene Options:", icon='SCENE_DATA') 463 | box.prop(self, "start") 464 | box.prop(self, "end") 465 | box.prop(self, "xsamples") 466 | box.prop(self, "gsamples") 467 | box.prop(self, "sh_open") 468 | box.prop(self, "sh_close") 469 | box.prop(self, "selected") 470 | box.prop(self, "renderable_only") 471 | box.prop(self, "visible_layers_only") 472 | box.prop(self, "flatten") 473 | 474 | box = layout.box() 475 | box.label(text="Object Options:", icon='OBJECT_DATA') 476 | box.prop(self, "uvs") 477 | box.prop(self, "packuv") 478 | box.prop(self, "normals") 479 | box.prop(self, "vcolors") 480 | box.prop(self, "face_sets") 481 | box.prop(self, "subdiv_schema") 482 | box.prop(self, "apply_subdiv") 483 | if bpy.app.version >= (2, 79, 0): 484 | box.prop(self, "triangulate") 485 | box.prop(self, "quad_method") 486 | box.prop(self, "ngon_method") 487 | 488 | if bpy.app.version >= (2, 79, 0): 489 | box = layout.box() 490 | box.label(text="Particle Systems:", icon='PARTICLES') 491 | box.prop(self, "export_hair") 492 | box.prop(self, "export_particles") 493 | 494 | # Most formats support only mesh geometry (not curve/text/metaball) 495 | exporter_specifics = { 496 | "wm.collada_export":dict(nonmesh=False, dupli=False, instancing=True), 497 | "export_scene.fbx":dict(nonmesh=False, dupli=False, instancing=True), 498 | "wm.alembic_export":dict(nonmesh=False, dupli=False, instancing=False), 499 | "export_scene.obj":dict(nonmesh=False, dupli=False, instancing=False), 500 | "export_scene.x3d":dict(nonmesh=False, dupli=False, instancing=False), 501 | "export_scene.x":dict(nonmesh=False, dupli=False, instancing=False), 502 | "export_scene.vrml2":dict(nonmesh=False, dupli=False, instancing=False), 503 | "export_scene.autodesk_3ds":dict(nonmesh=False, dupli=False, instancing=False), 504 | "export_scene.ms3d":dict(nonmesh=False, dupli=False, join=True), 505 | "export.dxf":dict(nonmesh=False, dupli=False, join=True), 506 | "export_mesh.ply":dict(nonmesh=False, dupli=False, join=True), 507 | "export_mesh.stl":dict(nonmesh=False, dupli=False, join=True), 508 | "export_mesh.raw":dict(nonmesh=False, dupli=False, join=True), 509 | "export_mesh.pdb":dict(nonmesh=False, dupli=False, join=True), # ? 510 | "export_mesh.paper_model":dict(nonmesh=False, dupli=False, join=True), 511 | } 512 | 513 | #============================================================================# 514 | 515 | class CurrentExporterProperties(bpy.types.PropertyGroup): 516 | __dict = {} 517 | __exporter = None 518 | 519 | @classmethod 520 | def _check(cls, exporter): 521 | return (cls.__exporter == exporter) 522 | 523 | @classmethod 524 | def _load_props(cls, exporter): 525 | if (cls.__exporter == exporter): return 526 | cls.__exporter = exporter 527 | 528 | CurrentExporterProperties.__dict = {} 529 | for key in list(cls._keys()): 530 | delattr(cls, key) 531 | 532 | template = get_exporter_class(exporter) 533 | if template: 534 | for key in dir(template): 535 | value = getattr(template, key) 536 | if is_bpy_prop(value): 537 | if not key.startswith("_"): setattr(cls, key, value) 538 | else: 539 | CurrentExporterProperties.__dict[key] = value 540 | 541 | @classmethod 542 | def _keys(cls, exclude_hidden=False): 543 | for kv in iter_public_bpy_props(cls, exclude_hidden): 544 | yield kv[0] 545 | 546 | def __getattr__(self, name): 547 | return CurrentExporterProperties.__dict[name] 548 | 549 | def __setattr__(self, name, value): 550 | if hasattr(self.__class__, name) and (not name.startswith("_")): 551 | supercls = super(CurrentExporterProperties, self.__class__) 552 | supercls.__setattr__(self, name, value) 553 | else: 554 | CurrentExporterProperties.__dict[name] = value 555 | 556 | def draw(self, context): 557 | if not CurrentExporterProperties.__dict: return 558 | 559 | _draw = CurrentExporterProperties.__dict.get("draw") 560 | if _draw: 561 | try: 562 | _draw(self, context) 563 | except: 564 | _draw = None 565 | del CurrentExporterProperties.__dict["draw"] 566 | 567 | if not _draw: 568 | ignore = {"filepath", "filename_ext", "filter_glob"} 569 | for key in CurrentExporterProperties._keys(True): 570 | if key in ignore: continue 571 | self.layout.prop(self, key) 572 | 573 | class ExportSelected_Base(ExportHelper): 574 | filename_ext = bpy.props.StringProperty(default="") 575 | filter_glob = bpy.props.StringProperty(default="*.*") 576 | 577 | __strings = {} 578 | __lock = PrimitiveLock() 579 | 580 | @staticmethod 581 | def __add_item(items, idname, name, description): 582 | # To avoid crash, references to Python strings must be kept alive 583 | # Seems like id/name/description have to be DIFFERENT OBJECTS, otherwise there will be glitches 584 | __strings = ExportSelected_Base.__strings 585 | idname = __strings.setdefault(idname, idname) 586 | name = __strings.setdefault(name, name) 587 | description = __strings.setdefault(description, description) 588 | items.append((idname, name, description)) 589 | 590 | def get_preset_items(self, context): 591 | items = [] 592 | preset_dir = bpy_path_join(operator_presets_dir, ExportSelected.bl_idname, "") 593 | if os.path.exists(preset_dir): 594 | for filename in os.listdir(preset_dir): 595 | if not os.path.isfile(bpy_path_join(preset_dir, filename)): continue 596 | name, ext = bpy_path_splitext(filename) 597 | if ext.lower() != ".json": continue 598 | ExportSelected_Base.__add_item(items, filename, name, name+" ") 599 | if not items: ExportSelected_Base.__add_item(items, '/NO_PRESETS/', "(No presets)", "") 600 | return items 601 | 602 | def update_preset(self, context): 603 | if self.preset_select == '/NO_PRESETS/': return 604 | 605 | preset_dir = bpy_path_join(operator_presets_dir, ExportSelected.bl_idname, "") 606 | preset_path = bpy_path_join(preset_dir, self.preset_select) 607 | 608 | if not os.path.isfile(preset_path): return 609 | 610 | try: 611 | with open(preset_path, "r") as f: 612 | json_data = json.loads(f.read()) 613 | except (IOError, json.decoder.JSONDecodeError): 614 | self.preset_name = "" 615 | try: 616 | os.remove(preset_path) 617 | except IOError: 618 | pass 619 | return 620 | 621 | self.preset_name = bpy_path_splitext(self.preset_select)[0] 622 | 623 | def value_convert(value): 624 | if isinstance(value, list): 625 | if not value: return set() 626 | first_item = value[0] 627 | return set(value) if isinstance(first_item, str) else tuple(value) 628 | return value 629 | 630 | exporter_data = json_data.pop("exporter_props", {}) 631 | 632 | for key, value in ExportSelected_Base.main_kwargs(self, True).items(): 633 | if key not in json_data: continue 634 | setattr(self, key, value_convert(json_data[key])) 635 | 636 | self.exporter = self.exporter_str 637 | 638 | for key, value in ExportSelected_Base.exporter_kwargs(self).items(): 639 | if key not in exporter_data: continue 640 | setattr(self.exporter_props, key, value_convert(exporter_data[key])) 641 | 642 | def update_preset_name(self, context): 643 | clean_name = clean_filename(self.preset_name) 644 | if self.preset_name != clean_name: self.preset_name = clean_name 645 | 646 | def save_preset(self, context): 647 | if not self.preset_save: return 648 | if not self.preset_name: return 649 | 650 | preset_dir = bpy_path_join(operator_presets_dir, ExportSelected.bl_idname, "") 651 | if not os.path.exists(preset_dir): os.makedirs(preset_dir) 652 | 653 | preset_path = bpy_path_join(preset_dir, self.preset_name+".json") 654 | 655 | exclude_keys = {"filepath", "filename_ext", "filter_glob", "check_existing"} 656 | 657 | def value_convert(value): 658 | if isinstance(value, set): return list(value) 659 | return value 660 | 661 | exporter_data = {} 662 | for key, value in ExportSelected_Base.exporter_kwargs(self).items(): 663 | if key in exclude_keys: continue 664 | exporter_data[key] = value_convert(value) 665 | 666 | json_data = {"exporter_props":exporter_data} 667 | for key, value in ExportSelected_Base.main_kwargs(self, True).items(): 668 | if key in exclude_keys: continue 669 | json_data[key] = value_convert(value) 670 | 671 | with open(preset_path, "w") as f: 672 | f.write(json.dumps(json_data, sort_keys=True, indent=4)) 673 | 674 | def delete_preset(self, context): 675 | if not self.preset_delete: return 676 | if not self.preset_name: return 677 | 678 | preset_dir = bpy_path_join(operator_presets_dir, ExportSelected.bl_idname, "") 679 | preset_path = bpy_path_join(preset_dir, self.preset_name+".json") 680 | if os.path.isfile(preset_path): os.remove(preset_path) 681 | 682 | self.preset_name = "" 683 | 684 | preset_select = bpy.props.EnumProperty(name="Select preset", description="Select preset", items=get_preset_items, update=update_preset, options={'HIDDEN'}) 685 | preset_name = bpy.props.StringProperty(name="Preset name", description="Preset name", default="", update=update_preset_name, options={'HIDDEN'}) 686 | preset_save = bpy.props.BoolProperty(name="Save preset", description="Save preset", default=False, update=save_preset, options={'HIDDEN'}) 687 | preset_delete = bpy.props.BoolProperty(name="Delete preset", description="Delete preset", default=False, update=delete_preset, options={'HIDDEN'}) 688 | 689 | bundle_mode = bpy.props.EnumProperty(name="Bundling", description="Export to multiple files", default='NONE', items=[ 690 | ('NONE', "Project", "No bundling (export to one file)", 'WORLD', 0), 691 | ('INDIVIDUAL', "Object", "Export each object separately", 'ROTATECOLLECTION', 1), 692 | ('ROOT', "Root", "Bundle by topmost parent", 'ARMATURE_DATA', 2), 693 | ('GROUP', "Group", "Bundle by group", 'GROUP', 3), 694 | ('LAYER', "Layer", "Bundle by layer", 'RENDERLAYERS', 4), 695 | ('MATERIAL', "Material", "Bundle by material", 'MATERIAL', 5), 696 | ]) 697 | 698 | include_hierarchy = bpy.props.EnumProperty(name="Include", description="What objects to include", default='CHILDREN', items=[ 699 | ('SELECTED', "Selected", "Selected objects", 'BONE_DATA', 0), 700 | ('CHILDREN', "Children", "Selected objects + their children", 'GROUP_BONE', 1), 701 | ('HIERARCHY', "Hierarchy", "Selected objects + their hierarchy", 'ARMATURE_DATA', 2), 702 | ('ALL', "All", "All objects", 'WORLD', 3), 703 | ]) 704 | include_invisible = bpy.props.BoolProperty(name="Invisible", description="Allow invisible objects", default=True) 705 | 706 | object_types = bpy.props.EnumProperty(name="Object types", description="Object type(s) to export", options={'ENUM_FLAG'}, default=set(object_types), items=[ 707 | ('MESH', "Mesh", "", 'OUTLINER_OB_MESH', 1 << 0), 708 | ('CURVE', "Curve", "", 'OUTLINER_OB_CURVE', 1 << 1), 709 | ('SURFACE', "Surface", "", 'OUTLINER_OB_SURFACE', 1 << 2), 710 | ('META', "Meta", "", 'OUTLINER_OB_META', 1 << 3), 711 | ('FONT', "Font", "", 'OUTLINER_OB_FONT', 1 << 4), 712 | ('ARMATURE', "Armature", "", 'OUTLINER_OB_ARMATURE', 1 << 5), 713 | ('LATTICE', "Lattice", "", 'OUTLINER_OB_LATTICE', 1 << 6), 714 | ('EMPTY', "Empty", "", 'OUTLINER_OB_EMPTY', 1 << 7), 715 | ('CAMERA', "Camera", "", 'OUTLINER_OB_CAMERA', 1 << 8), 716 | ('LAMP', "Lamp", "", 'OUTLINER_OB_LAMP', 1 << 9), 717 | ('SPEAKER', "Speaker", "", 'OUTLINER_OB_SPEAKER', 1 << 10), 718 | ]) 719 | 720 | centering_mode = bpy.props.EnumProperty(name="Centering", description="Centering", default='WORLD', items=[ 721 | ('WORLD', "World", "Center at world origin", 'MANIPUL', 0), 722 | ('ACTIVE_ELEMENT', "Active", "Center at active object", 'ROTACTIVE', 1), 723 | ('MEDIAN_POINT', "Average", "Center at the average position of exported objects", 'ROTATECENTER', 2), 724 | ('BOUNDING_BOX_CENTER', "Bounding box", "Center at the bounding box center of exported objects", 'ROTATE', 3), 725 | ('CURSOR', "Cursor", "Center at the 3D cursor", 'CURSOR', 4), 726 | ('INDIVIDUAL_ORIGINS', "Individual", "Center each exported object", 'ROTATECOLLECTION', 5), 727 | ]) 728 | 729 | preserve_dupli_hierarchy = bpy.props.BoolProperty(name="Preserve dupli hierarchy", description="Preserve dupli hierarchy", default=True) 730 | use_convert_dupli = bpy.props.BoolProperty(name="Dupli->real", description="Make duplicates real", default=False) 731 | use_convert_mesh = bpy.props.BoolProperty(name="To meshes", description="Convert to mesh(es)", default=False) 732 | 733 | exporter_infos = {} 734 | exporter_items = [('BLEND', "Blend", "")] # has to be non-empty 735 | def get_exporter_items(self, context): 736 | exporter_infos = ExportSelected_Base.exporter_infos 737 | exporter_items = ExportSelected_Base.exporter_items 738 | 739 | if ExportSelected_Base.__lock: return exporter_items 740 | with ExportSelected_Base.__lock: 741 | exporter_infos.clear() 742 | exporter_items.clear() 743 | for idname, name, filename_ext, filter_glob in iter_exporter_info(): 744 | exporter_infos[idname] = {"name":name, "ext":filename_ext, "glob":filter_glob, "index":len(exporter_items)} 745 | ExportSelected_Base.__add_item(exporter_items, idname, name, "Operator: "+idname) 746 | 747 | # If some exporter addon is enabled/disabled, the existing enum index must be updated 748 | if self.exporter_str in exporter_infos: 749 | if self.exporter_index != exporter_infos[self.exporter_str]["index"]: 750 | self.exporter = self.exporter_str 751 | else: 752 | self.exporter = exporter_items[0][0] 753 | 754 | return exporter_items 755 | 756 | def update_exporter(self, context): 757 | exporter = self.exporter 758 | is_same = (exporter == self.exporter_str) 759 | 760 | self.exporter_str = exporter 761 | exporter_info = ExportSelected_Base.exporter_infos.get(self.exporter_str, {}) 762 | self.exporter_index = exporter_info.get("index", -1) 763 | 764 | CurrentExporterProperties._load_props(self.exporter_str) 765 | self.filename_ext = exporter_info.get("ext", "") 766 | self.filter_glob = exporter_info.get("glob", "*") 767 | 768 | # Note: in file-browser mode it's impossible to alter the filepath after the invoke() 769 | self.filepath = replace_extension(self.filepath, self.filename_ext) 770 | 771 | exporter = bpy.props.EnumProperty(name="Exporter", description="Export format", items=get_exporter_items, update=update_exporter) 772 | exporter_str = bpy.props.StringProperty(default="", options={'HIDDEN'}) # an actual string value (enum is int) 773 | exporter_index = bpy.props.IntProperty(default=-1, options={'HIDDEN'}) # memorized index 774 | exporter_props = bpy.props.PointerProperty(type=CurrentExporterProperties) 775 | 776 | def abspath(self, path): 777 | format = self.exporter_infos[self.exporter]["name"] 778 | return bpy.path.abspath(path.format(format=format)) 779 | 780 | def generate_name(self, context=None): 781 | if not context: context = bpy.context 782 | file_dir = bpy_path_split(self.filepath)[0] 783 | objs = self.gather_objects(context.scene) 784 | roots = self.get_local_roots(objs) 785 | if len(roots) == 1: 786 | file_name = next(iter(roots)).name 787 | elif len(roots) == 0: 788 | file_name = "" 789 | else: 790 | file_name = bpy_path_basename(context.blend_data.filepath or "untitled") 791 | file_name = bpy_path_splitext(file_name)[0] 792 | if roots: file_name += "-"+hashnames(obj.name for obj in roots) 793 | if file_name: 794 | file_name = clean_filename(file_name) + self.filename_ext 795 | self.filepath = bpy_path_join(file_dir, file_name) 796 | 797 | def get_local_roots(self, objs): 798 | roots = set() 799 | for obj in objs: 800 | parents = set(obj_parents(obj)) 801 | if parents.isdisjoint(objs): roots.add(obj) 802 | return roots 803 | 804 | def can_include(self, obj, scene): 805 | return (obj.type in self.object_types) and (self.include_invisible or obj.is_visible(scene)) 806 | 807 | def gather_objects(self, scene): 808 | objs = set() 809 | 810 | def is_selected(obj): 811 | return obj.select and (not obj.hide) and (not obj.hide_select) and layers_intersect(obj, scene) and obj.is_visible(scene) 812 | 813 | def add_obj(obj): 814 | if obj in objs: return 815 | 816 | if self.can_include(obj, scene): objs.add(obj) 817 | 818 | if self.include_hierarchy in ('CHILDREN', 'HIERARCHY'): 819 | for child in obj.children: 820 | add_obj(child) 821 | 822 | for obj in scene.objects: 823 | if (self.include_hierarchy != 'ALL') and (not is_selected(obj)): continue 824 | if self.include_hierarchy == 'HIERARCHY': obj = obj_root(obj) 825 | add_obj(obj) 826 | 827 | return objs 828 | 829 | _main_kwargs_ignore = { 830 | "filename_ext", "filter_glob", "exporter", "exporter_index", "exporter_props", 831 | "preset_select", "preset_name", "preset_save", "preset_delete", 832 | } 833 | _main_kwargs_ignore_presets = { 834 | "bundle_mode", 835 | } 836 | def main_kwargs(self, for_preset=False): 837 | kwargs = {} 838 | for key, value in iter_public_bpy_props(ExportSelected_Base): # NOT self.__class__ 839 | if key in ExportSelected_Base._main_kwargs_ignore: continue 840 | if for_preset: 841 | if key in ExportSelected_Base._main_kwargs_ignore_presets: continue 842 | kwargs[key] = getattr(self, key) 843 | return kwargs 844 | 845 | def exporter_kwargs(self): 846 | kwargs = {key:getattr(self.exporter_props, key) for key in CurrentExporterProperties._keys()} 847 | kwargs["filepath"] = self.filepath 848 | return kwargs 849 | 850 | def draw(self, context): 851 | layout = self.layout 852 | 853 | if not CurrentExporterProperties._check(self.exporter_str): self.exporter = self.exporter_str 854 | 855 | row = layout.row(True) 856 | row.prop(self, "preset_select", text="", icon_only=True, icon='DOWNARROW_HLT') 857 | row.prop(self, "preset_name", text="") 858 | row.prop(self, "preset_save", text="", icon_only=True, icon=('FILE_TICK' if not self.preset_save else 'SAVE_AS'), toggle=True) 859 | row.prop(self, "preset_delete", text="", icon_only=True, icon=('X' if not self.preset_delete else 'PANEL_CLOSE'), toggle=True) 860 | 861 | row = layout.row(True) 862 | for obj_type in object_types: 863 | row.prop_enum(self, "object_types", obj_type, text="") 864 | 865 | row = layout.row(True) 866 | row.prop(self, "include_invisible", toggle=True, icon_only=True, icon='GHOST_ENABLED') 867 | row.prop(self, "include_hierarchy", text="") 868 | row.prop(self, "centering_mode", text="") 869 | 870 | row = layout.row(True) 871 | row.prop(self, "preserve_dupli_hierarchy", text="", icon='OOPS') 872 | row.prop(self, "use_convert_dupli", toggle=True) 873 | row.prop(self, "use_convert_mesh", toggle=True) 874 | 875 | box = layout.box() 876 | box.enabled = False 877 | 878 | self.exporter_props.layout = layout 879 | self.exporter_props.draw(context) 880 | 881 | if self.preset_save: self.preset_save = False 882 | if self.preset_delete: self.preset_delete = False 883 | 884 | class ExportSelected(bpy.types.Operator, ExportSelected_Base): 885 | '''Export selected objects to a chosen format''' 886 | bl_idname = "export_scene.selected" 887 | bl_label = "Export Selected" 888 | bl_options = {'REGISTER'} 889 | 890 | use_file_browser = bpy.props.BoolProperty(name="Use file browser", description="Use file browser", default=True) 891 | 892 | def center_objects(self, scene, objs): 893 | if self.centering_mode == 'WORLD': return 894 | if not objs: return 895 | 896 | if self.centering_mode == 'INDIVIDUAL_ORIGINS': 897 | center_pos = None 898 | elif self.centering_mode == 'CURSOR': 899 | center_pos = Vector(scene.cursor_location) 900 | elif self.centering_mode == 'ACTIVE_ELEMENT': 901 | obj = scene.objects.active 902 | center_pos = (Vector(obj.matrix_world.translation) if obj else None) 903 | elif self.centering_mode == 'MEDIAN_POINT': 904 | center_pos = Vector() 905 | for obj in objs: 906 | center_pos += obj.matrix_world.translation 907 | center_pos *= (1.0 / len(objs)) 908 | elif self.centering_mode == 'BOUNDING_BOX_CENTER': 909 | v_min, v_max = None, None 910 | for obj in objs: 911 | p = obj.matrix_world.translation 912 | if v_min is None: 913 | v_min = (p[0], p[1], p[2]) 914 | v_max = (p[0], p[1], p[2]) 915 | else: 916 | v_min = (min(p[0], v_min[0]), min(p[1], v_min[1]), min(p[2], v_min[2])) 917 | v_max = (max(p[0], v_max[0]), max(p[1], v_max[1]), max(p[2], v_max[2])) 918 | center_pos = (Vector(v_min) + Vector(v_max)) * 0.5 919 | 920 | roots = [obj for obj in objs if not obj.parent] 921 | for obj in roots: 922 | if center_pos is None: 923 | obj.matrix_world.translation = Vector() 924 | else: 925 | obj.matrix_world.translation -= center_pos 926 | 927 | scene.update() # required for children to actually update their matrices 928 | 929 | scene.cursor_location = Vector() # just to tidy up 930 | 931 | def convert_dupli(self, scene, objs): 932 | specifics = exporter_specifics.get(self.exporter, {}) 933 | use_convert_dupli = self.use_convert_dupli or (not specifics.get("dupli", True)) 934 | 935 | if not use_convert_dupli: return 936 | if not objs: return 937 | 938 | del_objs = {obj for obj in scene.objects if obj not in objs} 939 | 940 | if not self.preserve_dupli_hierarchy: 941 | for obj in scene.objects: 942 | obj.hide_select = False 943 | obj.select = obj in objs 944 | bpy.ops.object.duplicates_make_real(use_base_parent=False, use_hierarchy=False) 945 | else: 946 | for obj in objs: 947 | instantiate_duplis(obj, scene) 948 | 949 | for obj in scene.objects: 950 | if obj in del_objs: continue 951 | if self.can_include(obj, scene): objs.add(obj) 952 | 953 | def convert_mesh(self, scene, objs): 954 | specifics = exporter_specifics.get(self.exporter, {}) 955 | use_convert_mesh = self.use_convert_mesh or (not specifics.get("nonmesh", True)) 956 | 957 | if not use_convert_mesh: return 958 | if not objs: return 959 | 960 | for obj in scene.objects: 961 | obj.hide_select = False 962 | obj.select = obj in objs 963 | 964 | # For some reason object.convert() REQUIRES an active object to be present 965 | if scene.objects.active not in objs: scene.objects.active = next(iter(objs)) 966 | 967 | prev_objs = set(scene.objects) 968 | bpy.ops.object.convert(target='MESH') 969 | new_objs = set(scene.objects) - prev_objs 970 | 971 | for obj in new_objs: 972 | if self.can_include(obj, scene): objs.add(obj) 973 | 974 | def rename_data(self, scene, objs): 975 | addon_prefs = bpy.context.user_preferences.addons[__name__].preferences 976 | if not addon_prefs.rename_data: return 977 | if not objs: return 978 | 979 | specifics = exporter_specifics.get(self.exporter, {}) 980 | instancing = specifics.get("instancing", True) 981 | 982 | names = {} 983 | for obj in scene.objects: 984 | data = obj.data 985 | if not data: continue 986 | if (not instancing) and (data.users - int(data.use_fake_user) > 1): 987 | data = data.copy() 988 | obj.data = data 989 | name = names.get(data) 990 | if (name is None) or (len(obj.name) < len(name)): 991 | names[data] = obj.name 992 | 993 | for data, name in names.items(): 994 | data.name = name 995 | 996 | def delete_other_objects(self, scene, objs): 997 | del_objs = {obj for obj in scene.objects if obj not in objs} 998 | 999 | for obj in del_objs: 1000 | scene.objects.unlink(obj) 1001 | 1002 | # For non-.blend exporters, this is not necessary and may actually cause crashes 1003 | if self.exporter == 'BLEND': 1004 | while True: 1005 | n = len(del_objs) 1006 | for obj in tuple(del_objs): 1007 | try: 1008 | bpy.data.objects.remove(obj) 1009 | del_objs.discard(obj) 1010 | except RuntimeError: # non-zero users 1011 | pass 1012 | if len(del_objs) == n: break 1013 | 1014 | def find_mesh_obj(self, objs, obj): 1015 | if (obj in objs) and (obj.type == 'MESH'): return obj 1016 | for obj in objs: 1017 | if obj.type == 'MESH': return obj 1018 | return None 1019 | 1020 | def clear_world(self, context, objs): 1021 | specifics = exporter_specifics.get(self.exporter, {}) 1022 | 1023 | for scene in bpy.data.scenes: 1024 | if scene != context.scene: 1025 | try: 1026 | bpy.data.scenes.remove(scene, do_unlink=True) # Blender 2.78 1027 | except TypeError: 1028 | bpy.data.scenes.remove(scene) # earlier versions 1029 | 1030 | scene = context.scene 1031 | 1032 | self.center_objects(scene, objs) 1033 | 1034 | self.convert_dupli(scene, objs) 1035 | 1036 | self.convert_mesh(scene, objs) 1037 | 1038 | self.rename_data(scene, objs) 1039 | 1040 | matrix_map = {obj:Matrix(obj.matrix_world) for obj in objs} 1041 | 1042 | self.delete_other_objects(scene, objs) 1043 | 1044 | for obj, matrix in matrix_map.items(): 1045 | obj.hide_select = False 1046 | obj.select = True 1047 | obj.matrix_world = matrix 1048 | 1049 | scene.update() 1050 | 1051 | if specifics.get("join", False): 1052 | scene.objects.active = self.find_mesh_obj(objs, scene.objects.active) 1053 | if scene.objects.active: bpy.ops.object.join() 1054 | 1055 | def export(self, context): 1056 | dirpath = self.abspath(bpy_path_split(self.filepath)[0]) 1057 | if not os.path.exists(dirpath): os.makedirs(dirpath) 1058 | 1059 | addon_prefs = context.user_preferences.addons[__name__].preferences 1060 | 1061 | kwargs = self.exporter_kwargs() 1062 | 1063 | if self.exporter != 'BLEND': 1064 | op = get_op(self.exporter) 1065 | op(**kwargs) 1066 | # NOTE: For some reason, Alembic prevents undoing the effects 1067 | # of clear_world(), at least in Blender 2.78a. 1068 | # The user can undo manually, but doing it from script appears impossible. 1069 | else: 1070 | kwargs = {"compress":kwargs["compress"], "relative_remap":kwargs["relative_remap"]} 1071 | if hasattr(bpy.data.libraries, "write") and addon_prefs.save_blend_as_lib: 1072 | # Hopefully this does not save unused libraries: 1073 | refs = {context.scene} # {a, *b} syntax is only supported in recent Blender versions 1074 | refs.update(context.scene.objects) 1075 | bpy.data.libraries.write(self.filepath, refs, **kwargs) 1076 | else: 1077 | bpy.ops.wm.save_as_mainfile(filepath=self.filepath, copy=True, **kwargs) 1078 | 1079 | def export_bundle(self, context, filepath, bundle): 1080 | self.filepath = filepath 1081 | with ToggleObjectMode(undo=None): 1082 | edit_preferences = bpy.context.user_preferences.edit 1083 | use_global_undo = edit_preferences.use_global_undo 1084 | undo_steps = edit_preferences.undo_steps 1085 | undo_memory_limit = edit_preferences.undo_memory_limit 1086 | edit_preferences.use_global_undo = True 1087 | edit_preferences.undo_steps = max(undo_steps, 2) # just in case 1088 | edit_preferences.undo_memory_limit = 0 # unlimited 1089 | cursor_location = Vector(context.scene.cursor_location) 1090 | bpy.ops.ed.undo_push(message="Delete unselected") 1091 | self.clear_world(context, bundle) 1092 | self.export(context) 1093 | bpy.ops.ed.undo() 1094 | context.scene.cursor_location = cursor_location 1095 | edit_preferences.use_global_undo = use_global_undo 1096 | edit_preferences.undo_steps = undo_steps 1097 | edit_preferences.undo_memory_limit = undo_memory_limit 1098 | 1099 | def get_bundle_keys_individual(self, obj): 1100 | return {obj.name} 1101 | def get_bundle_keys_root(self, obj): 1102 | return {"Root="+obj_root(obj).name} 1103 | def get_bundle_keys_group(self, obj): 1104 | keys = {"Group="+group.name for group in bpy.data.groups if belongs_to_group(obj, group, True)} 1105 | return (keys if keys else {"Group="}) 1106 | def get_bundle_keys_layer(self, obj): 1107 | keys = {"Layer="+str(i) for i in range(len(obj.layers)) if obj.layers[i]} 1108 | return (keys if keys else {"Layer="}) 1109 | def get_bundle_keys_material(self, obj): 1110 | keys = {"Material="+slot.material.name for slot in obj.material_slots if slot.material} 1111 | return (keys if keys else {"Material="}) 1112 | 1113 | def resolve_key_conflicts(self, clean_keys): 1114 | fixed_keys = {ck for k, ck in clean_keys.items() if k == ck} 1115 | for k, ck in tuple((k, ck) for k, ck in clean_keys.items() if k != ck): 1116 | ck0 = ck 1117 | i = 1 1118 | while ck in fixed_keys: 1119 | ck = ck0 + "("+str(i)+")" 1120 | i += 1 1121 | fixed_keys.add(ck) 1122 | clean_keys[k] = ck 1123 | 1124 | def bundle_objects(self, objs): 1125 | basepath, ext = bpy_path_splitext(self.filepath) 1126 | if self.bundle_mode == 'NONE': 1127 | yield basepath+ext, objs 1128 | else: 1129 | keyfunc = getattr(self, "get_bundle_keys_"+self.bundle_mode.lower()) 1130 | clean_keys = {} 1131 | bundles_dict = {} 1132 | for obj in objs: 1133 | for key in keyfunc(obj): 1134 | clean_keys[key] = clean_filename(key) 1135 | bundles_dict.setdefault(key, []).append(obj.name) 1136 | self.resolve_key_conflicts(clean_keys) 1137 | if bpy_path_basename(basepath): basepath += "-" 1138 | for key, bundle in bundles_dict.items(): 1139 | # Due to Undo on export, object references will be invalid 1140 | bundle = {bpy.data.objects[obj_name] for obj_name in bundle} 1141 | yield basepath+clean_keys[key]+ext, bundle 1142 | 1143 | @classmethod 1144 | def poll(cls, context): 1145 | return len(context.scene.objects) != 0 1146 | 1147 | def invoke(self, context, event): 1148 | self.exporter = (self.exporter_str or self.exporter) # make sure properties are up-to-date 1149 | if self.use_file_browser: 1150 | self.filepath = context.blend_data.filepath or "untitled" 1151 | self.generate_name(context) 1152 | return ExportHelper.invoke(self, context, event) 1153 | else: 1154 | return self.execute(context) 1155 | 1156 | def execute(self, context): 1157 | objs = self.gather_objects(context.scene) 1158 | if not objs: 1159 | self.report({'ERROR_INVALID_CONTEXT'}, "No objects to export") 1160 | return {'CANCELLED'} 1161 | self.filepath = self.abspath(self.filepath).replace("/", os.path.sep) 1162 | for filepath, bundle in self.bundle_objects(objs): 1163 | self.export_bundle(context, filepath, bundle) 1164 | bpy.ops.ed.undo_push(message="Export Selected") 1165 | return {'FINISHED'} 1166 | 1167 | def draw(self, context): 1168 | layout = self.layout 1169 | 1170 | row = layout.row(True) 1171 | row.prop(self, "exporter", text="") 1172 | row.prop(self, "bundle_mode", text="") 1173 | 1174 | ExportSelected_Base.draw(self, context) 1175 | 1176 | class ExportSelectedPG(bpy.types.PropertyGroup, ExportSelected_Base): 1177 | # "//" is relative to current .blend file 1178 | filepath = bpy.props.StringProperty(default="//", subtype='FILE_PATH') 1179 | 1180 | def _get_filedir(self): 1181 | return bpy_path_split(self.filepath)[0] 1182 | def _set_filedir(self, value): 1183 | self.filepath = bpy_path_join(value, bpy_path_split(self.filepath)[1]) 1184 | filedir = bpy.props.StringProperty(description="Export directory (red when does not exist)", get=_get_filedir, set=_set_filedir, subtype='DIR_PATH') 1185 | 1186 | def _get_filename(self): 1187 | if self.auto_name: self.generate_name() 1188 | return bpy_path_split(self.filepath)[1] 1189 | def _set_filename(self, value): 1190 | self.auto_name = False 1191 | value = replace_extension(value, self.filename_ext) 1192 | value = clean_filename(value) 1193 | self.filepath = bpy_path_join(bpy_path_split(self.filepath)[0], value) 1194 | filename = bpy.props.StringProperty(description="File name (red when already exists)", get=_get_filename, set=_set_filename, subtype='FILE_NAME') 1195 | 1196 | auto_name = bpy.props.BoolProperty(name="Auto name", description="Auto-generate file name", default=True) 1197 | 1198 | def draw_export(self, row): 1199 | row2 = row.row(True) 1200 | row2.enabled = bool(self.filename) or (self.bundle_mode != 'NONE') 1201 | 1202 | op_info = row2.operator(ExportSelected.bl_idname, text="Export", icon='EXPORT') 1203 | op_info.use_file_browser = False 1204 | 1205 | for key, value in self.main_kwargs().items(): 1206 | setattr(op_info, key, value) 1207 | 1208 | for key, value in self.exporter_kwargs().items(): 1209 | setattr(op_info.exporter_props, key, value) 1210 | 1211 | def draw(self, context): 1212 | layout = self.layout 1213 | 1214 | dir_exists = os.path.exists(self.abspath(self.filedir)) 1215 | file_exists = os.path.exists(self.abspath(self.filepath)) 1216 | 1217 | column = layout.column(True) 1218 | 1219 | row = column.row(True) 1220 | row.alert = not dir_exists 1221 | row.prop(self, "filedir", text="") 1222 | 1223 | row = column.row(True) 1224 | row2 = row.row(True) 1225 | row2.alert = file_exists and (self.bundle_mode == 'NONE') 1226 | row2.prop(self, "filename", text="") 1227 | row.prop(self, "auto_name", text="", icon='SCENE_DATA', toggle=True) 1228 | 1229 | row = column.row(True) 1230 | row2 = row.row(True) 1231 | self.draw_export(row2) 1232 | row2.prop(self, "exporter", text="") 1233 | row2 = row.row(True) 1234 | row2.alignment = 'RIGHT' 1235 | row2.scale_x = 0.55 1236 | row2.prop(self, "bundle_mode", text="", icon_only=True, expand=False) 1237 | 1238 | ExportSelected_Base.draw(self, context) 1239 | 1240 | class ExportSelectedPanel(bpy.types.Panel): 1241 | bl_idname = "VIEW3D_PT_export_selected" 1242 | bl_label = "Export Selected" 1243 | bl_space_type = 'VIEW_3D' 1244 | bl_region_type = 'TOOLS' 1245 | bl_category = "Export" 1246 | 1247 | @classmethod 1248 | def poll(cls, context): 1249 | addon_prefs = context.user_preferences.addons[__name__].preferences 1250 | if not addon_prefs: return False # can this happen? 1251 | return addon_prefs.show_in_shelf 1252 | 1253 | def draw(self, context): 1254 | layout = self.layout 1255 | internal = get_internal_storage() 1256 | internal.layout = layout 1257 | internal.draw(context) 1258 | 1259 | class OBJECT_MT_selected_export(bpy.types.Menu): 1260 | bl_idname = "OBJECT_MT_selected_export" 1261 | bl_label = "Selected" 1262 | 1263 | def draw(self, context): 1264 | layout = self.layout 1265 | for idname, name, filename_ext, filter_glob in iter_exporter_info(): 1266 | row = layout.row() 1267 | if idname != 'BLEND': row.enabled = get_op(idname).poll() 1268 | op_info = row.operator(ExportSelected.bl_idname, text="{} ({})".format(name, filename_ext)) 1269 | op_info.exporter_str = idname 1270 | op_info.use_file_browser = True 1271 | 1272 | def menu(self, context): 1273 | self.layout.menu("OBJECT_MT_selected_export", text="Selected") 1274 | 1275 | class ExportSelectedPreferences(bpy.types.AddonPreferences): 1276 | # this must match the addon name, use '__package__' 1277 | # when defining this in a submodule of a python package. 1278 | bl_idname = __name__ 1279 | 1280 | show_in_shelf = bpy.props.BoolProperty(name="Show in shelf", default=False) 1281 | save_blend_as_lib = bpy.props.BoolProperty(name="Save .blend as a library", default=False, 1282 | description="The exported .blend will not contain unused libraries, but thumbnails also won't be generated") 1283 | rename_data = bpy.props.BoolProperty(name="Rename datablocks", default=False, 1284 | description="Rename datablocks to match the corresponding objects' names") 1285 | 1286 | def draw(self, context): 1287 | layout = self.layout 1288 | layout.prop(self, "show_in_shelf") 1289 | layout.prop(self, "save_blend_as_lib") 1290 | layout.prop(self, "rename_data") 1291 | 1292 | storage_name_internal = "<%s-internal-storage>" % "io_export_selected" 1293 | def get_internal_storage(): 1294 | screens = bpy.data.screens 1295 | screen = screens["Default" if ("Default" in screens) else 0] 1296 | return getattr(screen, storage_name_internal) 1297 | 1298 | def register(): 1299 | bpy.utils.register_class(CurrentExporterProperties) 1300 | bpy.utils.register_class(ExportSelectedPreferences) 1301 | bpy.utils.register_class(ExportSelectedPG) 1302 | bpy.utils.register_class(ExportSelectedPanel) 1303 | bpy.utils.register_class(ExportSelected) 1304 | bpy.utils.register_class(OBJECT_MT_selected_export) 1305 | bpy.types.INFO_MT_file_export.prepend(OBJECT_MT_selected_export.menu) 1306 | setattr(bpy.types.Screen, storage_name_internal, bpy.props.PointerProperty(type=ExportSelectedPG, options={'HIDDEN'})) 1307 | 1308 | def unregister(): 1309 | delattr(bpy.types.Screen, storage_name_internal) 1310 | bpy.types.INFO_MT_file_export.remove(OBJECT_MT_selected_export.menu) 1311 | bpy.utils.unregister_class(OBJECT_MT_selected_export) 1312 | bpy.utils.unregister_class(ExportSelected) 1313 | bpy.utils.unregister_class(ExportSelectedPanel) 1314 | bpy.utils.unregister_class(ExportSelectedPG) 1315 | bpy.utils.unregister_class(ExportSelectedPreferences) 1316 | bpy.utils.unregister_class(CurrentExporterProperties) 1317 | 1318 | if __name__ == "__main__": 1319 | register() 1320 | --------------------------------------------------------------------------------