import os import bpy VERBOSE = False # enable this for debugging def debug_print(*args): """Print debug messages""" if VERBOSE: print(*args) def relative_path_to_file(filepath): """Makes a path relative to the current file""" return bpy.path.relpath(filepath) def absolute_path_from_file(rel_filepath): return bpy.path.abspath(rel_filepath) def relative_path_to_lib(filepath): """Makes a path relative to the current library""" filepath = absolute_path_from_file(filepath) libpath = os.path.dirname( absolute_path_from_file(bpy.context.scene['lib_path'])) rel_path = os.path.relpath(filepath, libpath) return rel_path def bottom_up_from_idblock(idblock): """Generator, yields datablocks from the bottom (i.e. uses nothing) upward. Stupid in that it doesn't detect cycles yet. :param idblock: the idblock whose users to yield. """ visited = set() def visit(idblock): # Prevent visiting the same idblock multiple times if idblock in visited: return visited.add(idblock) user_map = bpy.data.user_map([idblock]) # There is only one entry here, for the idblock we requested. for user in user_map[idblock]: yield from visit(user) yield idblock yield from visit(idblock) def make_local(ob): # make local like a boss (using the patch from Sybren Stuvel) for idblock in bottom_up_from_idblock(ob): if idblock.library is None: # Already local continue debug_print('Should make %r local: ' % idblock) debug_print(' - result: %s' % idblock.make_local(clear_proxy=True)) # this shouldn't happen, but it does happen :/ if idblock.library: pass def treat_ob(ob, grp): """Remap existing ob to the new ob""" ob_name = ob.name debug_print(f'Processing {ob_name}') try: existing = bpy.data.objects[ob_name, None] except KeyError: debug_print('Not yet in Blender, just linking to scene.') bpy.context.scene.collection.objects.link(ob) make_local(ob) ob = bpy.data.objects[ob_name, None] debug_print('GRP: ', grp.name) grp.objects.link(ob) else: debug_print(f'Updating {ob.name}') # when an object already exists: # - find local version # - user_remap() it existing.user_remap(ob) existing.name = f'(PRE-SPLODE LOCAL) {existing.name}' # Preserve visible or hidden state ob.hide_viewport = existing.hide_viewport # Preserve animation (used to place the instance in the scene) if existing.animation_data: ob.animation_data_create() ob.animation_data.action = existing.animation_data.action bpy.data.objects.remove(existing) make_local(ob) def load_collection_reference_objects(filepath, collection_names): # We load one collection at a time debug_print(f'Loading collections {filepath} : {collection_names}') rel_path = relative_path_to_file(filepath) # Road a object scene we know the name of. with bpy.data.libraries.load(rel_path, link=True) as (data_from, data_to): data_to.collections = collection_names data = {} for collection in data_to.collections: debug_print(f'Handling collection {collection.name}') ref_collection_name = f'__REF{collection.name}' if ref_collection_name in bpy.data.collections: object_names_from = [ob.name for ob in collection.objects] object_names_to = [ ob.name for ob in bpy.data.collections[ref_collection_name].objects] object_names_diff = list( set(object_names_to) - set(object_names_from)) # Delete removed objects for ob in object_names_diff: # bpy.data.objects[ob].select_set(True) # bpy.ops.object.delete() bpy.data.objects.remove(bpy.data.objects[ob]) else: bpy.ops.collection.create(name=ref_collection_name) # store all the objects that are in the collection data[bpy.data.collections[ref_collection_name]] = [ ob for ob in collection.objects] # remove the collections bpy.data.collections.remove(collection, do_unlink=True) # add the new objects and make them local process_collection_reference_objects(data) def process_collection_reference_objects(data): for collection, objects in data.items(): for ob in objects: treat_ob(ob, collection) def load_instance_collections(filepath, collection_names): debug_print(f'Loading collections {filepath} : {collection_names}') rel_path = relative_path_to_file(filepath) # Load an object scene we know the name of. with bpy.data.libraries.load(rel_path, link=True) as (data_from, data_to): data_to.collections = collection_names scene = bpy.context.scene for collection in collection_names: instance = bpy.data.objects.new(collection.name, None) instance.instance_type = 'COLLECTION' instance.empty_display_size = 0.01 instance.instance_collection = collection scene.collection.objects.link(instance) def load_non_instance_collections(filepath, collection_names): debug_print(f'Loading collections {filepath} : {collection_names}') rel_path = relative_path_to_file(filepath) # Load an object scene we know the name of. with bpy.data.libraries.load(rel_path, link=True) as (data_from, data_to): data_to.collections = collection_names scene = bpy.context.scene for collection in collection_names: scene.collection.children.link(collection)