# ***** 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 3 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, see . # # ***** END GPL LICENSE BLOCK ***** bl_info = { "name": "Export Selected", "author": "dairin0d, rking, moth3r", "version": (2, 2, 2), "blender": (2, 7, 0), "location": "File > Export > Selected", "description": "Export selected objects to a chosen format", "warning": "", "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Import-Export/Export_Selected", "tracker_url": "https://github.com/dairin0d/export-selected/issues", "category": "Import-Export"} #============================================================================# # TODO: # * implement dynamic exporter properties differently (current implementation cannot support simultaneously opened export UIs if their export formats are different) # * batch import (Blender allows selecting multiple files, but only one of them is actually imported) import bpy from bpy_extras.io_utils import ExportHelper, ImportHelper from mathutils import Vector, Matrix, Quaternion, Euler from collections import namedtuple import os import json import re import hashlib def bpy_path_normslash(path): return path.replace(os.path.sep, "/") def bpy_path_join(*paths): # use os.path.join logic (it's not that simple) return bpy_path_normslash(os.path.join(*paths)) def bpy_path_splitext(path): path = bpy_path_normslash(path) i_split = path.rfind(".") if i_split < 0: return (path, "") return (path[:i_split], path[i_split:]) # For some reason, when path contains "//", os.path.split ignores single slashes # When path ends with slash, return dir without slash, except when it's / or // def bpy_path_split(path): path = bpy_path_normslash(path) i_split = path.rfind("/") + 1 dir_part = path[:i_split] file_part = path[i_split:] dir_part_strip = dir_part.rstrip("/") if dir_part_strip: dir_part = dir_part[:len(dir_part_strip)] return (dir_part, file_part) def bpy_path_dirname(path): return bpy_path_split(path)[0] def bpy_path_basename(path): return bpy_path_split(path)[1] operator_presets_dir = bpy_path_join(bpy.utils.resource_path('USER'), "scripts", "presets", "operator") object_types = ['MESH', 'CURVE', 'SURFACE', 'META', 'FONT', 'ARMATURE', 'LATTICE', 'EMPTY', 'CAMERA', 'LAMP', 'SPEAKER'] bpy_props = { bpy.props.BoolProperty, bpy.props.BoolVectorProperty, bpy.props.IntProperty, bpy.props.IntVectorProperty, bpy.props.FloatProperty, bpy.props.FloatVectorProperty, bpy.props.StringProperty, bpy.props.EnumProperty, bpy.props.PointerProperty, bpy.props.CollectionProperty, } def is_bpy_prop(value): return (isinstance(value, tuple) and (len(value) == 2) and (value[0] in bpy_props) and isinstance(value[1], dict)) def iter_public_bpy_props(cls, exclude_hidden=False): for key in dir(cls): if key.startswith("_"): continue value = getattr(cls, key) if not is_bpy_prop(value): continue if exclude_hidden: options = value[1].get("options", "") if 'HIDDEN' in options: continue yield (key, value) def get_op(idname): category_name, op_name = idname.split(".") category = getattr(bpy.ops, category_name) return getattr(category, op_name) def layers_intersect(a, b, name_a="layers", name_b=None): return any(l0 and l1 for l0, l1 in zip(getattr(a, name_a), getattr(b, name_b or name_a))) def obj_root(obj): while obj.parent: obj = obj.parent return obj def obj_parents(obj): while obj.parent: yield obj.parent obj = obj.parent def belongs_to_group(obj, group, consider_dupli=False): if not obj: return None # Object is either IN some group or INSTANTIATES that group, never both if obj.dupli_group == group: return ('DUPLI' if consider_dupli else None) elif obj.name in group.objects: return 'PART' return None # FRAMES copies the object itself, but not its children # VERTS and FACES copy the children # GROUP copies the group contents def get_dupli_roots(obj, scene=None, settings='VIEWPORT'): if (not obj) or (obj.dupli_type == 'NONE'): return None if not scene: scene = bpy.context.scene filter = None if obj.dupli_type in ('VERTS', 'FACES'): filter = set(obj.children) elif (obj.dupli_type == 'GROUP') and obj.dupli_group: filter = set(obj.dupli_group.objects) roots = [] if obj.dupli_list: obj.dupli_list_clear() obj.dupli_list_create(scene, settings) for dupli in obj.dupli_list: if (not filter) or (dupli.object in filter): roots.append((dupli.object, Matrix(dupli.matrix))) obj.dupli_list_clear() return roots def instantiate_duplis(obj, scene=None, settings='VIEWPORT', depth=-1): if (not obj) or (obj.dupli_type == 'NONE'): return if not scene: scene = bpy.context.scene if depth == 0: return if depth > 0: depth -= 1 roots = get_dupli_roots(obj, scene, settings) dupli_type = obj.dupli_type # Prevent recursive copying in FRAMES dupli mode obj.dupli_type = 'NONE' dst_info = [] src_dst = {} for src_obj, matrix in roots: dst_obj = src_obj.copy() dst_obj.constraints.clear() scene.objects.link(dst_obj) if dupli_type == 'FRAMES': dst_obj.animation_data_clear() dst_info.append((dst_obj, src_obj, matrix)) src_dst[src_obj] = dst_obj scene.update() # <-- important for dst_obj, src_obj, matrix in dst_info: dst_parent = src_dst.get(src_obj.parent) if dst_parent: # parent_type, parent_bone, parent_vertices # should be copied automatically dst_obj.parent = dst_parent else: dst_obj.parent_type = 'OBJECT' dst_obj.parent = obj for dst_obj, src_obj, matrix in dst_info: dst_obj.matrix_world = matrix for dst_obj, src_obj, matrix in dst_info: instantiate_duplis(dst_obj, scene, settings, depth) class PrimitiveLock(object): "Primary use of such lock is to prevent infinite recursion" def __init__(self): self.count = 0 def __bool__(self): return bool(self.count) def __enter__(self): self.count += 1 def __exit__(self, exc_type, exc_value, exc_traceback): self.count -= 1 class ToggleObjectMode: def __init__(self, mode='OBJECT', undo=False): if not isinstance(mode, str): mode = ('OBJECT' if mode else None) obj = bpy.context.object self.mode = (mode if obj and (obj.mode != mode) else None) self.undo = undo def __enter__(self): if self.mode: edit_preferences = bpy.context.user_preferences.edit self.global_undo = edit_preferences.use_global_undo # if self.mode == True, bpy.context.object exists self.prev_mode = bpy.context.object.mode if self.prev_mode != self.mode: if self.undo is not None: edit_preferences.use_global_undo = self.undo bpy.ops.object.mode_set(mode=self.mode) return self def __exit__(self, type, value, traceback): if self.mode: edit_preferences = bpy.context.user_preferences.edit if self.prev_mode != self.mode: bpy.ops.object.mode_set(mode=self.prev_mode) edit_preferences.use_global_undo = self.global_undo #============================================================================# # Adapted from https://gist.github.com/regularcoder/8254723 def fletcher(data, n): # n should be 16, 32 or 64 nbytes = min(max(n // 16, 1), 4) mod = 2 ** (8 * nbytes) - 1 sum1 = sum2 = 0 for i in range(0, len(data), nbytes): block = int.from_bytes(data[i:i + nbytes], 'little') sum1 = (sum1 + block) % mod sum2 = (sum2 + sum1) % mod return sum1 + (sum2 * (mod+1)) def hashnames(): hashnames_codes = [chr(o) for o in range(ord("0"), ord("9")+1)] hashnames_codes += [chr(o) for o in range(ord("A"), ord("Z")+1)] n = len(hashnames_codes) def _hashnames(names): binary_data = "\0".join(sorted(names)).encode() hash_value = fletcher(binary_data, 32) result = [] while True: k = hash_value % n result.append(hashnames_codes[k]) hash_value = (hash_value - k) // n if hash_value == 0: break return "".join(result) return _hashnames hashnames = hashnames() def replace_extension(path, ext): name = bpy_path_basename(path) if name and not name.lower().endswith(ext.lower()): path = bpy_path_splitext(path)[0] + ext return path forbidden_chars = "\x00-\x1f/" # on all OSes forbidden_chars += "<>:\"|?*\\\\" # on Windows/FAT/NTFS forbidden_chars = "["+forbidden_chars+"]" def clean_filename(filename, sub="-"): return re.sub(forbidden_chars, sub, filename) #============================================================================# def iter_exporters(): for category_name in dir(bpy.ops): if "export" not in category_name: continue op_category = getattr(bpy.ops, category_name) for name in dir(op_category): idname = category_name + "." + name if idname == ExportSelected.bl_idname: continue if "export" not in idname: continue yield (idname, getattr(op_category, name)) def get_instance_type_or_emulator(obj): if hasattr(obj, "get_instance"): return type(obj.get_instance()) rna_type = obj.get_rna_type() rna_props = rna_type.properties # namedtuple fields can't start with underscores, but so do rna props return namedtuple(rna_type.identifier, rna_props.keys())(*rna_props.values()) # For Blender 2.79.6 def get_rna_type(obj): if hasattr(obj, "rna_type"): return obj.rna_type if hasattr(obj, "get_rna"): return obj.get_rna().rna_type return obj.get_rna_type() # For Blender 2.79.6 def get_filter_glob(op, default_filter): if hasattr(op, "get_rna"): rna = op.get_rna() return getattr(rna, "filter_glob", default_filter) # There is no get_rna() in Blender 2.79.6 return op.get_rna_type().properties.get("filter_glob", default_filter) def iter_exporter_info(): # Special case: unconventional "exporter" yield ('BLEND', "Blend", ".blend", "*.blend") # Special case: unconventional operator name, ext/glob aren't exposed yield ('wm.collada_export', "Collada", ".dae", "*.dae") # Special case: unconventional operator name, ext/glob aren't exposed yield ('wm.alembic_export', "Alembic", ".abc", "*.abc") for idname, op in iter_exporters(): op_class = get_instance_type_or_emulator(op) if not hasattr(op_class, "filepath"): continue # e.g. sketchfab name = get_rna_type(op).name if name.lower().startswith("export "): name = name[len("export "):] filename_ext = getattr(op_class, "filename_ext", "") if not isinstance(filename_ext, str): # can be a bpy prop filename_ext = filename_ext[1].get("default", "") if not filename_ext: filename_ext = "."+idname.split(".")[-1] filter_glob = get_filter_glob(op, "*"+filename_ext) yield (idname, name, filename_ext, filter_glob) def get_exporter_name(idname): if idname == 'BLEND': return "Blend" op = get_op(idname) name = get_rna_type(op).name if name.lower().startswith("export "): name = name[len("export "):] return name def get_exporter_class(idname): if idname == 'BLEND': return BlendExportEmulator elif idname == 'wm.collada_export': return ColladaExportEmulator elif idname == 'wm.alembic_export': return AlembicExportEmulator else: op = get_op(idname) return get_instance_type_or_emulator(op) class BlendExportEmulator: # Special case: Blend compress = bpy.props.BoolProperty(name="Compress", description="Write compressed .blend file", default=False) relative_remap = bpy.props.BoolProperty(name="Remap Relative", description="Remap relative paths when saving in a different directory", default=True) class ColladaExportEmulator: # Special case: Collada (built-in) -- has no explicitly defined Python properties apply_modifiers = bpy.props.BoolProperty(name="Apply Modifiers", description="Apply modifiers to exported mesh (non destructive)", default=False) 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")]) selected = bpy.props.BoolProperty(name="Selection Only", description="Export only selected elements", default=False) include_children = bpy.props.BoolProperty(name="Include Children", description="Export all children of selected objects (even if not selected)", default=False) include_armatures = bpy.props.BoolProperty(name="Include Armatures", description="Export related armatures (even if not selected)", default=False) include_shapekeys = bpy.props.BoolProperty(name="Include Shape Keys", description="Export all Shape Keys from Mesh Objects", default=True) active_uv_only = bpy.props.BoolProperty(name="Only Active UV layer", description="Export textures assigned to the object UV maps", default=False) if bpy.app.version < (2, 79, 0): include_uv_textures = bpy.props.BoolProperty(name="Include UV Textures", description="Export textures assigned to the object UV maps", default=False) include_material_textures = bpy.props.BoolProperty(name="Include Material Textures", description="Export textures assigned to the object Materials", default=False) if bpy.app.version >= (2, 79, 0): 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")]) use_texture_copies = bpy.props.BoolProperty(name="Copy Textures", description="Copy textures to the same folder where .dae file is exported", default=True) deform_bones_only = bpy.props.BoolProperty(name="Deform Bones only", description="Only export deforming bones with armatures", default=False) open_sim = bpy.props.BoolProperty(name="Export for OpenSim", description="Compatibility mode for OpenSim and compatible online worlds", default=False) triangulate = bpy.props.BoolProperty(name="Triangulate", description="Export Polygons (Quads & NGons) as Triangles", default=True) use_object_instantiation = bpy.props.BoolProperty(name="Use Object Instances", description="Instantiate multiple Objects from same Data", default=True) 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")]) sort_by_name = bpy.props.BoolProperty(name="Sort by Object name", description="Sort exported data by Object name", default=False) if bpy.app.version >= (2, 79, 0): 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) limit_precision = bpy.props.BoolProperty(name="Limit Precision", description="Reduce the precision of the exported data to 6 digits", default=False) def draw(self, context): layout = self.layout box = layout.box() box.label(text="Export Data Options", icon='MESH_DATA') row = box.split(0.6) row.prop(self, "apply_modifiers") row.prop(self, "export_mesh_type_selection", text="") box.prop(self, "selected") box.prop(self, "include_children") box.prop(self, "include_armatures") box.prop(self, "include_shapekeys") box = layout.box() box.label(text="Texture Options", icon='TEXTURE') box.prop(self, "active_uv_only") if bpy.app.version < (2, 79, 0): box.prop(self, "include_uv_textures") box.prop(self, "include_material_textures") if bpy.app.version >= (2, 79, 0): box.prop(self, "export_texture_type_selection", text="") box.prop(self, "use_texture_copies", text="Copy") box = layout.box() box.label(text="Armature Options", icon='ARMATURE_DATA') box.prop(self, "deform_bones_only") box.prop(self, "open_sim") box = layout.box() box.label(text="Collada Options", icon='MODIFIER') box.prop(self, "triangulate") box.prop(self, "use_object_instantiation") row = box.split(0.6) row.label(text="Transformation Type") row.prop(self, "export_transformation_type_selection", text="") box.prop(self, "sort_by_name") if bpy.app.version >= (2, 79, 0): box.prop(self, "keep_bind_info") box.prop(self, "limit_precision") class AlembicExportEmulator: # Special case: Alembic (built-in) -- has no explicitly defined Python properties 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) start = bpy.props.IntProperty(name="Start Frame", description="Start Frame", default=1) end = bpy.props.IntProperty(name="End Frame", description="End Frame", default=1) xsamples = bpy.props.IntProperty(name="Transform Samples", description="Number of times per frame transformations are sampled", default=1, min=1, max=128) gsamples = bpy.props.IntProperty(name="Geometry Samples", description="Number of times per frame object data are sampled", default=1, min=1, max=128) 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) 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) selected = bpy.props.BoolProperty(name="Selected Objects Only", description="Export only selected objects", default=False) renderable_only = bpy.props.BoolProperty(name="Renderable Objects Only", description="Export only objects marked renderable in the outliner", default=True) visible_layers_only = bpy.props.BoolProperty(name="Visible Layers Only", description="Export only objects in visible layers", default=False) flatten = bpy.props.BoolProperty(name="Flatten Hierarchy", description="Do not preserve objects' parent/children relationship", default=False) uvs = bpy.props.BoolProperty(name="UVs", description="Export UVs", default=True) packuv = bpy.props.BoolProperty(name="Pack UV Islands", description="Export UVs with packed island", default=True) normals = bpy.props.BoolProperty(name="Normals", description="Export normals", default=True) vcolors = bpy.props.BoolProperty(name="Vertex Colors", description="Export vertex colors", default=False) face_sets = bpy.props.BoolProperty(name="Face Sets", description="Export per face shading group assignments", default=False) subdiv_schema = bpy.props.BoolProperty(name="Use Subdivision Schema", description="Export meshes using Alembic's subdivision schema", default=False) apply_subdiv = bpy.props.BoolProperty(name="Apply Subsurf", description="Export subdivision surfaces as meshes", default=False) if bpy.app.version >= (2, 79, 0): triangulate = bpy.props.BoolProperty(name="Triangulate", description="Export Polygons (Quads & NGons) as Triangles", default=False) 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.")]) 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.")]) export_hair = bpy.props.BoolProperty(name="Export Hair", description="Exports hair particle systems as animated curves", default=True) export_particles = bpy.props.BoolProperty(name="Export Particles", description="Exports non-hair particle systems", default=True) def draw(self, context): layout = self.layout box = layout.box() box.label(text="Manual Transform:") box.prop(self, "global_scale") box = layout.box() box.label(text="Scene Options:", icon='SCENE_DATA') box.prop(self, "start") box.prop(self, "end") box.prop(self, "xsamples") box.prop(self, "gsamples") box.prop(self, "sh_open") box.prop(self, "sh_close") box.prop(self, "selected") box.prop(self, "renderable_only") box.prop(self, "visible_layers_only") box.prop(self, "flatten") box = layout.box() box.label(text="Object Options:", icon='OBJECT_DATA') box.prop(self, "uvs") box.prop(self, "packuv") box.prop(self, "normals") box.prop(self, "vcolors") box.prop(self, "face_sets") box.prop(self, "subdiv_schema") box.prop(self, "apply_subdiv") if bpy.app.version >= (2, 79, 0): box.prop(self, "triangulate") box.prop(self, "quad_method") box.prop(self, "ngon_method") if bpy.app.version >= (2, 79, 0): box = layout.box() box.label(text="Particle Systems:", icon='PARTICLES') box.prop(self, "export_hair") box.prop(self, "export_particles") # Most formats support only mesh geometry (not curve/text/metaball) exporter_specifics = { "wm.collada_export":dict(nonmesh=False, dupli=False, instancing=True), "export_scene.fbx":dict(nonmesh=False, dupli=False, instancing=True), "wm.alembic_export":dict(nonmesh=False, dupli=False, instancing=False), "export_scene.obj":dict(nonmesh=False, dupli=False, instancing=False), "export_scene.x3d":dict(nonmesh=False, dupli=False, instancing=False), "export_scene.x":dict(nonmesh=False, dupli=False, instancing=False), "export_scene.vrml2":dict(nonmesh=False, dupli=False, instancing=False), "export_scene.autodesk_3ds":dict(nonmesh=False, dupli=False, instancing=False), "export_scene.ms3d":dict(nonmesh=False, dupli=False, join=True), "export.dxf":dict(nonmesh=False, dupli=False, join=True), "export_mesh.ply":dict(nonmesh=False, dupli=False, join=True), "export_mesh.stl":dict(nonmesh=False, dupli=False, join=True), "export_mesh.raw":dict(nonmesh=False, dupli=False, join=True), "export_mesh.pdb":dict(nonmesh=False, dupli=False, join=True), # ? "export_mesh.paper_model":dict(nonmesh=False, dupli=False, join=True), } #============================================================================# class CurrentExporterProperties(bpy.types.PropertyGroup): __dict = {} __exporter = None @classmethod def _check(cls, exporter): return (cls.__exporter == exporter) @classmethod def _load_props(cls, exporter): if (cls.__exporter == exporter): return cls.__exporter = exporter CurrentExporterProperties.__dict = {} for key in list(cls._keys()): delattr(cls, key) template = get_exporter_class(exporter) if template: for key in dir(template): value = getattr(template, key) if is_bpy_prop(value): if not key.startswith("_"): setattr(cls, key, value) else: CurrentExporterProperties.__dict[key] = value @classmethod def _keys(cls, exclude_hidden=False): for kv in iter_public_bpy_props(cls, exclude_hidden): yield kv[0] def __getattr__(self, name): return CurrentExporterProperties.__dict[name] def __setattr__(self, name, value): if hasattr(self.__class__, name) and (not name.startswith("_")): supercls = super(CurrentExporterProperties, self.__class__) supercls.__setattr__(self, name, value) else: CurrentExporterProperties.__dict[name] = value def draw(self, context): if not CurrentExporterProperties.__dict: return _draw = CurrentExporterProperties.__dict.get("draw") if _draw: try: _draw(self, context) except: _draw = None del CurrentExporterProperties.__dict["draw"] if not _draw: ignore = {"filepath", "filename_ext", "filter_glob"} for key in CurrentExporterProperties._keys(True): if key in ignore: continue self.layout.prop(self, key) class ExportSelected_Base(ExportHelper): filename_ext = bpy.props.StringProperty(default="") filter_glob = bpy.props.StringProperty(default="*.*") __strings = {} __lock = PrimitiveLock() @staticmethod def __add_item(items, idname, name, description): # To avoid crash, references to Python strings must be kept alive # Seems like id/name/description have to be DIFFERENT OBJECTS, otherwise there will be glitches __strings = ExportSelected_Base.__strings idname = __strings.setdefault(idname, idname) name = __strings.setdefault(name, name) description = __strings.setdefault(description, description) items.append((idname, name, description)) def get_preset_items(self, context): items = [] preset_dir = bpy_path_join(operator_presets_dir, ExportSelected.bl_idname, "") if os.path.exists(preset_dir): for filename in os.listdir(preset_dir): if not os.path.isfile(bpy_path_join(preset_dir, filename)): continue name, ext = bpy_path_splitext(filename) if ext.lower() != ".json": continue ExportSelected_Base.__add_item(items, filename, name, name+" ") if not items: ExportSelected_Base.__add_item(items, '/NO_PRESETS/', "(No presets)", "") return items def update_preset(self, context): if self.preset_select == '/NO_PRESETS/': return preset_dir = bpy_path_join(operator_presets_dir, ExportSelected.bl_idname, "") preset_path = bpy_path_join(preset_dir, self.preset_select) if not os.path.isfile(preset_path): return try: with open(preset_path, "r") as f: json_data = json.loads(f.read()) except (IOError, json.decoder.JSONDecodeError): self.preset_name = "" try: os.remove(preset_path) except IOError: pass return self.preset_name = bpy_path_splitext(self.preset_select)[0] def value_convert(value): if isinstance(value, list): if not value: return set() first_item = value[0] return set(value) if isinstance(first_item, str) else tuple(value) return value exporter_data = json_data.pop("exporter_props", {}) for key, value in ExportSelected_Base.main_kwargs(self, True).items(): if key not in json_data: continue setattr(self, key, value_convert(json_data[key])) self.exporter = self.exporter_str for key, value in ExportSelected_Base.exporter_kwargs(self).items(): if key not in exporter_data: continue setattr(self.exporter_props, key, value_convert(exporter_data[key])) def update_preset_name(self, context): clean_name = clean_filename(self.preset_name) if self.preset_name != clean_name: self.preset_name = clean_name def save_preset(self, context): if not self.preset_save: return if not self.preset_name: return preset_dir = bpy_path_join(operator_presets_dir, ExportSelected.bl_idname, "") if not os.path.exists(preset_dir): os.makedirs(preset_dir) preset_path = bpy_path_join(preset_dir, self.preset_name+".json") exclude_keys = {"filepath", "filename_ext", "filter_glob", "check_existing"} def value_convert(value): if isinstance(value, set): return list(value) return value exporter_data = {} for key, value in ExportSelected_Base.exporter_kwargs(self).items(): if key in exclude_keys: continue exporter_data[key] = value_convert(value) json_data = {"exporter_props":exporter_data} for key, value in ExportSelected_Base.main_kwargs(self, True).items(): if key in exclude_keys: continue json_data[key] = value_convert(value) with open(preset_path, "w") as f: f.write(json.dumps(json_data, sort_keys=True, indent=4)) def delete_preset(self, context): if not self.preset_delete: return if not self.preset_name: return preset_dir = bpy_path_join(operator_presets_dir, ExportSelected.bl_idname, "") preset_path = bpy_path_join(preset_dir, self.preset_name+".json") if os.path.isfile(preset_path): os.remove(preset_path) self.preset_name = "" preset_select = bpy.props.EnumProperty(name="Select preset", description="Select preset", items=get_preset_items, update=update_preset, options={'HIDDEN'}) preset_name = bpy.props.StringProperty(name="Preset name", description="Preset name", default="", update=update_preset_name, options={'HIDDEN'}) preset_save = bpy.props.BoolProperty(name="Save preset", description="Save preset", default=False, update=save_preset, options={'HIDDEN'}) preset_delete = bpy.props.BoolProperty(name="Delete preset", description="Delete preset", default=False, update=delete_preset, options={'HIDDEN'}) bundle_mode = bpy.props.EnumProperty(name="Bundling", description="Export to multiple files", default='NONE', items=[ ('NONE', "Project", "No bundling (export to one file)", 'WORLD', 0), ('INDIVIDUAL', "Object", "Export each object separately", 'ROTATECOLLECTION', 1), ('ROOT', "Root", "Bundle by topmost parent", 'ARMATURE_DATA', 2), ('GROUP', "Group", "Bundle by group", 'GROUP', 3), ('LAYER', "Layer", "Bundle by layer", 'RENDERLAYERS', 4), ('MATERIAL', "Material", "Bundle by material", 'MATERIAL', 5), ]) include_hierarchy = bpy.props.EnumProperty(name="Include", description="What objects to include", default='CHILDREN', items=[ ('SELECTED', "Selected", "Selected objects", 'BONE_DATA', 0), ('CHILDREN', "Children", "Selected objects + their children", 'GROUP_BONE', 1), ('HIERARCHY', "Hierarchy", "Selected objects + their hierarchy", 'ARMATURE_DATA', 2), ('ALL', "All", "All objects", 'WORLD', 3), ]) include_invisible = bpy.props.BoolProperty(name="Invisible", description="Allow invisible objects", default=True) object_types = bpy.props.EnumProperty(name="Object types", description="Object type(s) to export", options={'ENUM_FLAG'}, default=set(object_types), items=[ ('MESH', "Mesh", "", 'OUTLINER_OB_MESH', 1 << 0), ('CURVE', "Curve", "", 'OUTLINER_OB_CURVE', 1 << 1), ('SURFACE', "Surface", "", 'OUTLINER_OB_SURFACE', 1 << 2), ('META', "Meta", "", 'OUTLINER_OB_META', 1 << 3), ('FONT', "Font", "", 'OUTLINER_OB_FONT', 1 << 4), ('ARMATURE', "Armature", "", 'OUTLINER_OB_ARMATURE', 1 << 5), ('LATTICE', "Lattice", "", 'OUTLINER_OB_LATTICE', 1 << 6), ('EMPTY', "Empty", "", 'OUTLINER_OB_EMPTY', 1 << 7), ('CAMERA', "Camera", "", 'OUTLINER_OB_CAMERA', 1 << 8), ('LAMP', "Lamp", "", 'OUTLINER_OB_LAMP', 1 << 9), ('SPEAKER', "Speaker", "", 'OUTLINER_OB_SPEAKER', 1 << 10), ]) centering_mode = bpy.props.EnumProperty(name="Centering", description="Centering", default='WORLD', items=[ ('WORLD', "World", "Center at world origin", 'MANIPUL', 0), ('ACTIVE_ELEMENT', "Active", "Center at active object", 'ROTACTIVE', 1), ('MEDIAN_POINT', "Average", "Center at the average position of exported objects", 'ROTATECENTER', 2), ('BOUNDING_BOX_CENTER', "Bounding box", "Center at the bounding box center of exported objects", 'ROTATE', 3), ('CURSOR', "Cursor", "Center at the 3D cursor", 'CURSOR', 4), ('INDIVIDUAL_ORIGINS', "Individual", "Center each exported object", 'ROTATECOLLECTION', 5), ]) preserve_dupli_hierarchy = bpy.props.BoolProperty(name="Preserve dupli hierarchy", description="Preserve dupli hierarchy", default=True) use_convert_dupli = bpy.props.BoolProperty(name="Dupli->real", description="Make duplicates real", default=False) use_convert_mesh = bpy.props.BoolProperty(name="To meshes", description="Convert to mesh(es)", default=False) exporter_infos = {} exporter_items = [('BLEND', "Blend", "")] # has to be non-empty def get_exporter_items(self, context): exporter_infos = ExportSelected_Base.exporter_infos exporter_items = ExportSelected_Base.exporter_items if ExportSelected_Base.__lock: return exporter_items with ExportSelected_Base.__lock: exporter_infos.clear() exporter_items.clear() for idname, name, filename_ext, filter_glob in iter_exporter_info(): exporter_infos[idname] = {"name":name, "ext":filename_ext, "glob":filter_glob, "index":len(exporter_items)} ExportSelected_Base.__add_item(exporter_items, idname, name, "Operator: "+idname) # If some exporter addon is enabled/disabled, the existing enum index must be updated if self.exporter_str in exporter_infos: if self.exporter_index != exporter_infos[self.exporter_str]["index"]: self.exporter = self.exporter_str else: self.exporter = exporter_items[0][0] return exporter_items def update_exporter(self, context): exporter = self.exporter is_same = (exporter == self.exporter_str) self.exporter_str = exporter exporter_info = ExportSelected_Base.exporter_infos.get(self.exporter_str, {}) self.exporter_index = exporter_info.get("index", -1) CurrentExporterProperties._load_props(self.exporter_str) self.filename_ext = exporter_info.get("ext", "") self.filter_glob = exporter_info.get("glob", "*") # Note: in file-browser mode it's impossible to alter the filepath after the invoke() self.filepath = replace_extension(self.filepath, self.filename_ext) exporter = bpy.props.EnumProperty(name="Exporter", description="Export format", items=get_exporter_items, update=update_exporter) exporter_str = bpy.props.StringProperty(default="", options={'HIDDEN'}) # an actual string value (enum is int) exporter_index = bpy.props.IntProperty(default=-1, options={'HIDDEN'}) # memorized index exporter_props = bpy.props.PointerProperty(type=CurrentExporterProperties) def abspath(self, path): format = self.exporter_infos[self.exporter]["name"] return bpy.path.abspath(path.format(format=format)) def generate_name(self, context=None): if not context: context = bpy.context file_dir = bpy_path_split(self.filepath)[0] objs = self.gather_objects(context.scene) roots = self.get_local_roots(objs) if len(roots) == 1: file_name = next(iter(roots)).name elif len(roots) == 0: file_name = "" else: file_name = bpy_path_basename(context.blend_data.filepath or "untitled") file_name = bpy_path_splitext(file_name)[0] if roots: file_name += "-"+hashnames(obj.name for obj in roots) if file_name: file_name = clean_filename(file_name) + self.filename_ext self.filepath = bpy_path_join(file_dir, file_name) def get_local_roots(self, objs): roots = set() for obj in objs: parents = set(obj_parents(obj)) if parents.isdisjoint(objs): roots.add(obj) return roots def can_include(self, obj, scene): return (obj.type in self.object_types) and (self.include_invisible or obj.is_visible(scene)) def gather_objects(self, scene): objs = set() def is_selected(obj): return obj.select and (not obj.hide) and (not obj.hide_select) and layers_intersect(obj, scene) and obj.is_visible(scene) def add_obj(obj): if obj in objs: return if self.can_include(obj, scene): objs.add(obj) if self.include_hierarchy in ('CHILDREN', 'HIERARCHY'): for child in obj.children: add_obj(child) for obj in scene.objects: if (self.include_hierarchy != 'ALL') and (not is_selected(obj)): continue if self.include_hierarchy == 'HIERARCHY': obj = obj_root(obj) add_obj(obj) return objs _main_kwargs_ignore = { "filename_ext", "filter_glob", "exporter", "exporter_index", "exporter_props", "preset_select", "preset_name", "preset_save", "preset_delete", } _main_kwargs_ignore_presets = { "bundle_mode", } def main_kwargs(self, for_preset=False): kwargs = {} for key, value in iter_public_bpy_props(ExportSelected_Base): # NOT self.__class__ if key in ExportSelected_Base._main_kwargs_ignore: continue if for_preset: if key in ExportSelected_Base._main_kwargs_ignore_presets: continue kwargs[key] = getattr(self, key) return kwargs def exporter_kwargs(self): kwargs = {key:getattr(self.exporter_props, key) for key in CurrentExporterProperties._keys()} kwargs["filepath"] = self.filepath return kwargs def draw(self, context): layout = self.layout if not CurrentExporterProperties._check(self.exporter_str): self.exporter = self.exporter_str row = layout.row(True) row.prop(self, "preset_select", text="", icon_only=True, icon='DOWNARROW_HLT') row.prop(self, "preset_name", text="") row.prop(self, "preset_save", text="", icon_only=True, icon=('FILE_TICK' if not self.preset_save else 'SAVE_AS'), toggle=True) row.prop(self, "preset_delete", text="", icon_only=True, icon=('X' if not self.preset_delete else 'PANEL_CLOSE'), toggle=True) row = layout.row(True) for obj_type in object_types: row.prop_enum(self, "object_types", obj_type, text="") row = layout.row(True) row.prop(self, "include_invisible", toggle=True, icon_only=True, icon='GHOST_ENABLED') row.prop(self, "include_hierarchy", text="") row.prop(self, "centering_mode", text="") row = layout.row(True) row.prop(self, "preserve_dupli_hierarchy", text="", icon='OOPS') row.prop(self, "use_convert_dupli", toggle=True) row.prop(self, "use_convert_mesh", toggle=True) box = layout.box() box.enabled = False self.exporter_props.layout = layout self.exporter_props.draw(context) if self.preset_save: self.preset_save = False if self.preset_delete: self.preset_delete = False class ExportSelected(bpy.types.Operator, ExportSelected_Base): '''Export selected objects to a chosen format''' bl_idname = "export_scene.selected" bl_label = "Export Selected" bl_options = {'REGISTER'} use_file_browser = bpy.props.BoolProperty(name="Use file browser", description="Use file browser", default=True) def center_objects(self, scene, objs): if self.centering_mode == 'WORLD': return if not objs: return if self.centering_mode == 'INDIVIDUAL_ORIGINS': center_pos = None elif self.centering_mode == 'CURSOR': center_pos = Vector(scene.cursor_location) elif self.centering_mode == 'ACTIVE_ELEMENT': obj = scene.objects.active center_pos = (Vector(obj.matrix_world.translation) if obj else None) elif self.centering_mode == 'MEDIAN_POINT': center_pos = Vector() for obj in objs: center_pos += obj.matrix_world.translation center_pos *= (1.0 / len(objs)) elif self.centering_mode == 'BOUNDING_BOX_CENTER': v_min, v_max = None, None for obj in objs: p = obj.matrix_world.translation if v_min is None: v_min = (p[0], p[1], p[2]) v_max = (p[0], p[1], p[2]) else: v_min = (min(p[0], v_min[0]), min(p[1], v_min[1]), min(p[2], v_min[2])) v_max = (max(p[0], v_max[0]), max(p[1], v_max[1]), max(p[2], v_max[2])) center_pos = (Vector(v_min) + Vector(v_max)) * 0.5 roots = [obj for obj in objs if not obj.parent] for obj in roots: if center_pos is None: obj.matrix_world.translation = Vector() else: obj.matrix_world.translation -= center_pos scene.update() # required for children to actually update their matrices scene.cursor_location = Vector() # just to tidy up def convert_dupli(self, scene, objs): specifics = exporter_specifics.get(self.exporter, {}) use_convert_dupli = self.use_convert_dupli or (not specifics.get("dupli", True)) if not use_convert_dupli: return if not objs: return del_objs = {obj for obj in scene.objects if obj not in objs} if not self.preserve_dupli_hierarchy: for obj in scene.objects: obj.hide_select = False obj.select = obj in objs bpy.ops.object.duplicates_make_real(use_base_parent=False, use_hierarchy=False) else: for obj in objs: instantiate_duplis(obj, scene) for obj in scene.objects: if obj in del_objs: continue if self.can_include(obj, scene): objs.add(obj) def convert_mesh(self, scene, objs): specifics = exporter_specifics.get(self.exporter, {}) use_convert_mesh = self.use_convert_mesh or (not specifics.get("nonmesh", True)) if not use_convert_mesh: return if not objs: return for obj in scene.objects: obj.hide_select = False obj.select = obj in objs # For some reason object.convert() REQUIRES an active object to be present if scene.objects.active not in objs: scene.objects.active = next(iter(objs)) prev_objs = set(scene.objects) bpy.ops.object.convert(target='MESH') new_objs = set(scene.objects) - prev_objs for obj in new_objs: if self.can_include(obj, scene): objs.add(obj) def rename_data(self, scene, objs): addon_prefs = bpy.context.user_preferences.addons[__name__].preferences if not addon_prefs.rename_data: return if not objs: return specifics = exporter_specifics.get(self.exporter, {}) instancing = specifics.get("instancing", True) names = {} for obj in scene.objects: data = obj.data if not data: continue if (not instancing) and (data.users - int(data.use_fake_user) > 1): data = data.copy() obj.data = data name = names.get(data) if (name is None) or (len(obj.name) < len(name)): names[data] = obj.name for data, name in names.items(): data.name = name def delete_other_objects(self, scene, objs): del_objs = {obj for obj in scene.objects if obj not in objs} for obj in del_objs: scene.objects.unlink(obj) # For non-.blend exporters, this is not necessary and may actually cause crashes if self.exporter == 'BLEND': while True: n = len(del_objs) for obj in tuple(del_objs): try: bpy.data.objects.remove(obj) del_objs.discard(obj) except RuntimeError: # non-zero users pass if len(del_objs) == n: break def find_mesh_obj(self, objs, obj): if (obj in objs) and (obj.type == 'MESH'): return obj for obj in objs: if obj.type == 'MESH': return obj return None def clear_world(self, context, objs): specifics = exporter_specifics.get(self.exporter, {}) for scene in bpy.data.scenes: if scene != context.scene: try: bpy.data.scenes.remove(scene, do_unlink=True) # Blender 2.78 except TypeError: bpy.data.scenes.remove(scene) # earlier versions scene = context.scene self.center_objects(scene, objs) self.convert_dupli(scene, objs) self.convert_mesh(scene, objs) self.rename_data(scene, objs) matrix_map = {obj:Matrix(obj.matrix_world) for obj in objs} self.delete_other_objects(scene, objs) for obj, matrix in matrix_map.items(): obj.hide_select = False obj.select = True obj.matrix_world = matrix scene.update() if specifics.get("join", False): scene.objects.active = self.find_mesh_obj(objs, scene.objects.active) if scene.objects.active: bpy.ops.object.join() def export(self, context): dirpath = self.abspath(bpy_path_split(self.filepath)[0]) if not os.path.exists(dirpath): os.makedirs(dirpath) addon_prefs = context.user_preferences.addons[__name__].preferences kwargs = self.exporter_kwargs() if self.exporter != 'BLEND': op = get_op(self.exporter) op(**kwargs) # NOTE: For some reason, Alembic prevents undoing the effects # of clear_world(), at least in Blender 2.78a. # The user can undo manually, but doing it from script appears impossible. else: kwargs = {"compress":kwargs["compress"], "relative_remap":kwargs["relative_remap"]} if hasattr(bpy.data.libraries, "write") and addon_prefs.save_blend_as_lib: # Hopefully this does not save unused libraries: refs = {context.scene} # {a, *b} syntax is only supported in recent Blender versions refs.update(context.scene.objects) bpy.data.libraries.write(self.filepath, refs, **kwargs) else: bpy.ops.wm.save_as_mainfile(filepath=self.filepath, copy=True, **kwargs) def export_bundle(self, context, filepath, bundle): self.filepath = filepath with ToggleObjectMode(undo=None): edit_preferences = bpy.context.user_preferences.edit use_global_undo = edit_preferences.use_global_undo undo_steps = edit_preferences.undo_steps undo_memory_limit = edit_preferences.undo_memory_limit edit_preferences.use_global_undo = True edit_preferences.undo_steps = max(undo_steps, 2) # just in case edit_preferences.undo_memory_limit = 0 # unlimited cursor_location = Vector(context.scene.cursor_location) bpy.ops.ed.undo_push(message="Delete unselected") self.clear_world(context, bundle) self.export(context) bpy.ops.ed.undo() context.scene.cursor_location = cursor_location edit_preferences.use_global_undo = use_global_undo edit_preferences.undo_steps = undo_steps edit_preferences.undo_memory_limit = undo_memory_limit def get_bundle_keys_individual(self, obj): return {obj.name} def get_bundle_keys_root(self, obj): return {"Root="+obj_root(obj).name} def get_bundle_keys_group(self, obj): keys = {"Group="+group.name for group in bpy.data.groups if belongs_to_group(obj, group, True)} return (keys if keys else {"Group="}) def get_bundle_keys_layer(self, obj): keys = {"Layer="+str(i) for i in range(len(obj.layers)) if obj.layers[i]} return (keys if keys else {"Layer="}) def get_bundle_keys_material(self, obj): keys = {"Material="+slot.material.name for slot in obj.material_slots if slot.material} return (keys if keys else {"Material="}) def resolve_key_conflicts(self, clean_keys): fixed_keys = {ck for k, ck in clean_keys.items() if k == ck} for k, ck in tuple((k, ck) for k, ck in clean_keys.items() if k != ck): ck0 = ck i = 1 while ck in fixed_keys: ck = ck0 + "("+str(i)+")" i += 1 fixed_keys.add(ck) clean_keys[k] = ck def bundle_objects(self, objs): basepath, ext = bpy_path_splitext(self.filepath) if self.bundle_mode == 'NONE': yield basepath+ext, objs else: keyfunc = getattr(self, "get_bundle_keys_"+self.bundle_mode.lower()) clean_keys = {} bundles_dict = {} for obj in objs: for key in keyfunc(obj): clean_keys[key] = clean_filename(key) bundles_dict.setdefault(key, []).append(obj.name) self.resolve_key_conflicts(clean_keys) if bpy_path_basename(basepath): basepath += "-" for key, bundle in bundles_dict.items(): # Due to Undo on export, object references will be invalid bundle = {bpy.data.objects[obj_name] for obj_name in bundle} yield basepath+clean_keys[key]+ext, bundle @classmethod def poll(cls, context): return len(context.scene.objects) != 0 def invoke(self, context, event): self.exporter = (self.exporter_str or self.exporter) # make sure properties are up-to-date if self.use_file_browser: self.filepath = context.blend_data.filepath or "untitled" self.generate_name(context) return ExportHelper.invoke(self, context, event) else: return self.execute(context) def execute(self, context): objs = self.gather_objects(context.scene) if not objs: self.report({'ERROR_INVALID_CONTEXT'}, "No objects to export") return {'CANCELLED'} self.filepath = self.abspath(self.filepath).replace("/", os.path.sep) for filepath, bundle in self.bundle_objects(objs): self.export_bundle(context, filepath, bundle) bpy.ops.ed.undo_push(message="Export Selected") return {'FINISHED'} def draw(self, context): layout = self.layout row = layout.row(True) row.prop(self, "exporter", text="") row.prop(self, "bundle_mode", text="") ExportSelected_Base.draw(self, context) class ExportSelectedPG(bpy.types.PropertyGroup, ExportSelected_Base): # "//" is relative to current .blend file filepath = bpy.props.StringProperty(default="//", subtype='FILE_PATH') def _get_filedir(self): return bpy_path_split(self.filepath)[0] def _set_filedir(self, value): self.filepath = bpy_path_join(value, bpy_path_split(self.filepath)[1]) filedir = bpy.props.StringProperty(description="Export directory (red when does not exist)", get=_get_filedir, set=_set_filedir, subtype='DIR_PATH') def _get_filename(self): if self.auto_name: self.generate_name() return bpy_path_split(self.filepath)[1] def _set_filename(self, value): self.auto_name = False value = replace_extension(value, self.filename_ext) value = clean_filename(value) self.filepath = bpy_path_join(bpy_path_split(self.filepath)[0], value) filename = bpy.props.StringProperty(description="File name (red when already exists)", get=_get_filename, set=_set_filename, subtype='FILE_NAME') auto_name = bpy.props.BoolProperty(name="Auto name", description="Auto-generate file name", default=True) def draw_export(self, row): row2 = row.row(True) row2.enabled = bool(self.filename) or (self.bundle_mode != 'NONE') op_info = row2.operator(ExportSelected.bl_idname, text="Export", icon='EXPORT') op_info.use_file_browser = False for key, value in self.main_kwargs().items(): setattr(op_info, key, value) for key, value in self.exporter_kwargs().items(): setattr(op_info.exporter_props, key, value) def draw(self, context): layout = self.layout dir_exists = os.path.exists(self.abspath(self.filedir)) file_exists = os.path.exists(self.abspath(self.filepath)) column = layout.column(True) row = column.row(True) row.alert = not dir_exists row.prop(self, "filedir", text="") row = column.row(True) row2 = row.row(True) row2.alert = file_exists and (self.bundle_mode == 'NONE') row2.prop(self, "filename", text="") row.prop(self, "auto_name", text="", icon='SCENE_DATA', toggle=True) row = column.row(True) row2 = row.row(True) self.draw_export(row2) row2.prop(self, "exporter", text="") row2 = row.row(True) row2.alignment = 'RIGHT' row2.scale_x = 0.55 row2.prop(self, "bundle_mode", text="", icon_only=True, expand=False) ExportSelected_Base.draw(self, context) class ExportSelectedPanel(bpy.types.Panel): bl_idname = "VIEW3D_PT_export_selected" bl_label = "Export Selected" bl_space_type = 'VIEW_3D' bl_region_type = 'TOOLS' bl_category = "Export" @classmethod def poll(cls, context): addon_prefs = context.user_preferences.addons[__name__].preferences if not addon_prefs: return False # can this happen? return addon_prefs.show_in_shelf def draw(self, context): layout = self.layout internal = get_internal_storage() internal.layout = layout internal.draw(context) class OBJECT_MT_selected_export(bpy.types.Menu): bl_idname = "OBJECT_MT_selected_export" bl_label = "Selected" def draw(self, context): layout = self.layout for idname, name, filename_ext, filter_glob in iter_exporter_info(): row = layout.row() if idname != 'BLEND': row.enabled = get_op(idname).poll() op_info = row.operator(ExportSelected.bl_idname, text="{} ({})".format(name, filename_ext)) op_info.exporter_str = idname op_info.use_file_browser = True def menu(self, context): self.layout.menu("OBJECT_MT_selected_export", text="Selected") class ExportSelectedPreferences(bpy.types.AddonPreferences): # this must match the addon name, use '__package__' # when defining this in a submodule of a python package. bl_idname = __name__ show_in_shelf = bpy.props.BoolProperty(name="Show in shelf", default=False) save_blend_as_lib = bpy.props.BoolProperty(name="Save .blend as a library", default=False, description="The exported .blend will not contain unused libraries, but thumbnails also won't be generated") rename_data = bpy.props.BoolProperty(name="Rename datablocks", default=False, description="Rename datablocks to match the corresponding objects' names") def draw(self, context): layout = self.layout layout.prop(self, "show_in_shelf") layout.prop(self, "save_blend_as_lib") layout.prop(self, "rename_data") storage_name_internal = "<%s-internal-storage>" % "io_export_selected" def get_internal_storage(): screens = bpy.data.screens screen = screens["Default" if ("Default" in screens) else 0] return getattr(screen, storage_name_internal) def register(): bpy.utils.register_class(CurrentExporterProperties) bpy.utils.register_class(ExportSelectedPreferences) bpy.utils.register_class(ExportSelectedPG) bpy.utils.register_class(ExportSelectedPanel) bpy.utils.register_class(ExportSelected) bpy.utils.register_class(OBJECT_MT_selected_export) bpy.types.INFO_MT_file_export.prepend(OBJECT_MT_selected_export.menu) setattr(bpy.types.Screen, storage_name_internal, bpy.props.PointerProperty(type=ExportSelectedPG, options={'HIDDEN'})) def unregister(): delattr(bpy.types.Screen, storage_name_internal) bpy.types.INFO_MT_file_export.remove(OBJECT_MT_selected_export.menu) bpy.utils.unregister_class(OBJECT_MT_selected_export) bpy.utils.unregister_class(ExportSelected) bpy.utils.unregister_class(ExportSelectedPanel) bpy.utils.unregister_class(ExportSelectedPG) bpy.utils.unregister_class(ExportSelectedPreferences) bpy.utils.unregister_class(CurrentExporterProperties) if __name__ == "__main__": register()