├── 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 |
--------------------------------------------------------------------------------