diff --git a/pype/hosts/blender/plugin.py b/pype/hosts/blender/plugin.py index 77fce90d656..ab53d490416 100644 --- a/pype/hosts/blender/plugin.py +++ b/pype/hosts/blender/plugin.py @@ -14,12 +14,41 @@ def asset_name( asset: str, subset: str, namespace: Optional[str] = None ) -> str: """Return a consistent name for an asset.""" - name = f"{asset}_{subset}" + name = f"{asset}" if namespace: - name = f"{namespace}:{name}" + name = f"{name}_{namespace}" + name = f"{name}_{subset}" return name +def get_unique_number( + asset: str, subset: str +) -> str: + """Return a unique number based on the asset name.""" + avalon_containers = [ + c for c in bpy.data.collections + if c.name == 'AVALON_CONTAINERS' + ] + loaded_assets = [] + for c in avalon_containers: + loaded_assets.extend(c.children) + collections_names = [ + c.name for c in loaded_assets + ] + count = 1 + name = f"{asset}_{count:0>2}_{subset}_CON" + while name in collections_names: + count += 1 + name = f"{asset}_{count:0>2}_{subset}_CON" + return f"{count:0>2}" + + +def prepare_data(data, container_name): + name = data.name + data = data.make_local() + data.name = f"{name}:{container_name}" + + def create_blender_context(active: Optional[bpy.types.Object] = None, selected: Optional[bpy.types.Object] = None,): """Create a new Blender context. If an object is passed as @@ -47,6 +76,25 @@ def create_blender_context(active: Optional[bpy.types.Object] = None, raise Exception("Could not create a custom Blender context.") +def get_parent_collection(collection): + """Get the parent of the input collection""" + check_list = [bpy.context.scene.collection] + + for c in check_list: + if collection.name in c.children.keys(): + return c + check_list.extend(c.children) + + return None + + +def get_local_collection_with_name(name): + for collection in bpy.data.collections: + if collection.name == name and collection.library is None: + return collection + return None + + class AssetLoader(api.Loader): """A basic AssetLoader for Blender diff --git a/pype/plugins/blender/load/load_layout.py b/pype/plugins/blender/load/load_layout.py index 0c1032c4fb5..cfab5a207b2 100644 --- a/pype/plugins/blender/load/load_layout.py +++ b/pype/plugins/blender/load/load_layout.py @@ -7,20 +7,11 @@ from avalon import api, blender import bpy -import pype.hosts.blender.plugin +import pype.hosts.blender.plugin as plugin -logger = logging.getLogger("pype").getChild( - "blender").getChild("load_layout") - - -class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): - """Load animations from a .blend file. - - Warning: - Loading the same asset more then once is not properly supported at the - moment. - """ +class BlendLayoutLoader(plugin.AssetLoader): + """Load layout from a .blend file.""" families = ["layout"] representations = ["blend"] @@ -29,24 +20,25 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): icon = "code-fork" color = "orange" - def _remove(self, objects, lib_container): - + def _remove(self, objects, obj_container): for obj in objects: - if obj.type == 'ARMATURE': bpy.data.armatures.remove(obj.data) elif obj.type == 'MESH': bpy.data.meshes.remove(obj.data) + elif obj.type == 'CAMERA': + bpy.data.cameras.remove(obj.data) + elif obj.type == 'CURVE': + bpy.data.curves.remove(obj.data) - for element_container in bpy.data.collections[lib_container].children: + for element_container in obj_container.children: for child in element_container.children: bpy.data.collections.remove(child) bpy.data.collections.remove(element_container) - bpy.data.collections.remove(bpy.data.collections[lib_container]) + bpy.data.collections.remove(obj_container) def _process(self, libpath, lib_container, container_name, actions): - relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( libpath, link=True, relative=relative @@ -58,26 +50,38 @@ def _process(self, libpath, lib_container, container_name, actions): scene.collection.children.link(bpy.data.collections[lib_container]) layout_container = scene.collection.children[lib_container].make_local() + layout_container.name = container_name - meshes = [] + objects_local_types = ['MESH', 'CAMERA', 'CURVE'] + + objects = [] armatures = [] - objects_list = [] + containers = list(layout_container.children) - for element_container in layout_container.children: - element_container.make_local() - meshes.extend([obj for obj in element_container.objects if obj.type == 'MESH']) - armatures.extend([obj for obj in element_container.objects if obj.type == 'ARMATURE']) - for child in element_container.children: - child.make_local() - meshes.extend(child.objects) + for container in layout_container.children: + if container.name == blender.pipeline.AVALON_CONTAINERS: + containers.remove(container) + + for container in containers: + container.make_local() + objects.extend([ + obj for obj in container.objects + if obj.type in objects_local_types + ]) + armatures.extend([ + obj for obj in container.objects + if obj.type == 'ARMATURE' + ]) + containers.extend(list(container.children)) # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. - for obj in meshes + armatures: - obj = obj.make_local() - obj.data.make_local() + for obj in objects + armatures: + obj.make_local() + if obj.data: + obj.data.make_local() if not obj.get(blender.pipeline.AVALON_PROPERTY): obj[blender.pipeline.AVALON_PROPERTY] = dict() @@ -85,18 +89,16 @@ def _process(self, libpath, lib_container, container_name, actions): avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) - action = actions.get( obj.name, None ) + action = actions.get(obj.name, None) if obj.type == 'ARMATURE' and action is not None: obj.animation_data.action = action - objects_list.append(obj) - layout_container.pop(blender.pipeline.AVALON_PROPERTY) bpy.ops.object.select_all(action='DESELECT') - return objects_list + return layout_container def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, @@ -113,9 +115,15 @@ def process_asset( libpath = self.fname asset = context["asset"]["name"] subset = context["subset"]["name"] - lib_container = pype.hosts.blender.plugin.asset_name(asset, subset) - container_name = pype.hosts.blender.plugin.asset_name( - asset, subset, namespace + lib_container = plugin.asset_name( + asset, subset + ) + unique_number = plugin.get_unique_number( + asset, subset + ) + namespace = namespace or f"{asset}_{unique_number}" + container_name = plugin.asset_name( + asset, subset, unique_number ) container = bpy.data.collections.new(lib_container) @@ -134,11 +142,13 @@ def process_asset( container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - objects_list = self._process( + obj_container = self._process( libpath, lib_container, container_name, {}) + container_metadata["obj_container"] = obj_container + # Save the list of objects in the metadata container - container_metadata["objects"] = objects_list + container_metadata["objects"] = obj_container.all_objects nodes = list(container.objects) nodes.append(container) @@ -157,7 +167,6 @@ def update(self, container: Dict, representation: Dict): Warning: No nested collections are supported at the moment! """ - collection = bpy.data.collections.get( container["objectName"] ) @@ -165,7 +174,7 @@ def update(self, container: Dict, representation: Dict): libpath = Path(api.get_representation_path(representation)) extension = libpath.suffix.lower() - logger.info( + self.log.info( "Container: %s\nRepresentation: %s", pformat(container, indent=2), pformat(representation, indent=2), @@ -189,41 +198,40 @@ def update(self, container: Dict, representation: Dict): collection_metadata = collection.get( blender.pipeline.AVALON_PROPERTY) - collection_libpath = collection_metadata["libpath"] + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + obj_container = collection_metadata["obj_container"] + normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) normalized_libpath = ( str(Path(bpy.path.abspath(str(libpath))).resolve()) ) - logger.debug( + self.log.debug( "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", normalized_collection_libpath, normalized_libpath, ) if normalized_collection_libpath == normalized_libpath: - logger.info("Library already loaded, not updating...") + self.log.info("Library already loaded, not updating...") return - objects = collection_metadata["objects"] - lib_container = collection_metadata["lib_container"] - actions = {} for obj in objects: - if obj.type == 'ARMATURE': - actions[obj.name] = obj.animation_data.action - self._remove(objects, lib_container) + self._remove(objects, obj_container) - objects_list = self._process( + obj_container = self._process( str(libpath), lib_container, collection.name, actions) # Save the list of objects in the metadata container - collection_metadata["objects"] = objects_list + collection_metadata["obj_container"] = obj_container + collection_metadata["objects"] = obj_container.all_objects collection_metadata["libpath"] = str(libpath) collection_metadata["representation"] = str(representation["_id"]) @@ -255,9 +263,9 @@ def remove(self, container: Dict) -> bool: collection_metadata = collection.get( blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] - lib_container = collection_metadata["lib_container"] + obj_container = collection_metadata["obj_container"] - self._remove(objects, lib_container) + self._remove(objects, obj_container) bpy.data.collections.remove(collection) diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index 4a8f43cd48d..ad9137a15d7 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -7,20 +7,14 @@ from avalon import api, blender import bpy -import pype.hosts.blender.plugin +import pype.hosts.blender.plugin as plugin -logger = logging.getLogger("pype").getChild("blender").getChild("load_model") - -class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): +class BlendModelLoader(plugin.AssetLoader): """Load models from a .blend file. Because they come from a .blend file we can simply link the collection that contains the model. There is no further need to 'containerise' it. - - Warning: - Loading the same asset more then once is not properly supported at the - moment. """ families = ["model"] @@ -30,54 +24,52 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): icon = "code-fork" color = "orange" - def _remove(self, objects, lib_container): - + def _remove(self, objects, container): for obj in objects: - + for material_slot in obj.material_slots: + bpy.data.materials.remove(material_slot.material) bpy.data.meshes.remove(obj.data) - bpy.data.collections.remove(bpy.data.collections[lib_container]) - - def _process(self, libpath, lib_container, container_name): + bpy.data.collections.remove(container) + def _process( + self, libpath, lib_container, container_name, + parent_collection + ): relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( libpath, link=True, relative=relative ) as (_, data_to): data_to.collections = [lib_container] - scene = bpy.context.scene + parent = parent_collection - scene.collection.children.link(bpy.data.collections[lib_container]) + if parent is None: + parent = bpy.context.scene.collection - model_container = scene.collection.children[lib_container].make_local() + parent.children.link(bpy.data.collections[lib_container]) - objects_list = [] + model_container = parent.children[lib_container].make_local() + model_container.name = container_name for obj in model_container.objects: - - obj = obj.make_local() - - obj.data.make_local() + plugin.prepare_data(obj, container_name) + plugin.prepare_data(obj.data, container_name) for material_slot in obj.material_slots: - - material_slot.material.make_local() + plugin.prepare_data(material_slot.material, container_name) if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[blender.pipeline.AVALON_PROPERTY] = dict() avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) - objects_list.append(obj) - model_container.pop(blender.pipeline.AVALON_PROPERTY) bpy.ops.object.select_all(action='DESELECT') - return objects_list + return model_container def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, @@ -94,35 +86,44 @@ def process_asset( libpath = self.fname asset = context["asset"]["name"] subset = context["subset"]["name"] - lib_container = pype.hosts.blender.plugin.asset_name(asset, subset) - container_name = pype.hosts.blender.plugin.asset_name( - asset, subset, namespace + + lib_container = plugin.asset_name( + asset, subset + ) + unique_number = plugin.get_unique_number( + asset, subset + ) + namespace = namespace or f"{asset}_{unique_number}" + container_name = plugin.asset_name( + asset, subset, unique_number ) - collection = bpy.data.collections.new(lib_container) - collection.name = container_name + container = bpy.data.collections.new(lib_container) + container.name = container_name blender.pipeline.containerise_existing( - collection, + container, name, namespace, context, self.__class__.__name__, ) - container_metadata = collection.get( + container_metadata = container.get( blender.pipeline.AVALON_PROPERTY) container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - objects_list = self._process( - libpath, lib_container, container_name) + obj_container = self._process( + libpath, lib_container, container_name, None) + + container_metadata["obj_container"] = obj_container # Save the list of objects in the metadata container - container_metadata["objects"] = objects_list + container_metadata["objects"] = obj_container.all_objects - nodes = list(collection.objects) - nodes.append(collection) + nodes = list(container.objects) + nodes.append(container) self[:] = nodes return nodes @@ -144,7 +145,7 @@ def update(self, container: Dict, representation: Dict): libpath = Path(api.get_representation_path(representation)) extension = libpath.suffix.lower() - logger.debug( + self.log.info( "Container: %s\nRepresentation: %s", pformat(container, indent=2), pformat(representation, indent=2), @@ -162,38 +163,47 @@ def update(self, container: Dict, representation: Dict): assert libpath.is_file(), ( f"The file doesn't exist: {libpath}" ) - assert extension in pype.hosts.blender.plugin.VALID_EXTENSIONS, ( + assert extension in plugin.VALID_EXTENSIONS, ( f"Unsupported file: {libpath}" ) collection_metadata = collection.get( blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] - objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] + obj_container = plugin.get_local_collection_with_name( + collection_metadata["obj_container"].name + ) + objects = obj_container.all_objects + + container_name = obj_container.name + normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) normalized_libpath = ( str(Path(bpy.path.abspath(str(libpath))).resolve()) ) - logger.debug( + self.log.debug( "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", normalized_collection_libpath, normalized_libpath, ) if normalized_collection_libpath == normalized_libpath: - logger.info("Library already loaded, not updating...") + self.log.info("Library already loaded, not updating...") return - self._remove(objects, lib_container) + parent = plugin.get_parent_collection(obj_container) + + self._remove(objects, obj_container) - objects_list = self._process( - str(libpath), lib_container, collection.name) + obj_container = self._process( + str(libpath), lib_container, container_name, parent) # Save the list of objects in the metadata container - collection_metadata["objects"] = objects_list + collection_metadata["obj_container"] = obj_container + collection_metadata["objects"] = obj_container.all_objects collection_metadata["libpath"] = str(libpath) collection_metadata["representation"] = str(representation["_id"]) @@ -221,17 +231,20 @@ def remove(self, container: Dict) -> bool: collection_metadata = collection.get( blender.pipeline.AVALON_PROPERTY) - objects = collection_metadata["objects"] - lib_container = collection_metadata["lib_container"] - self._remove(objects, lib_container) + obj_container = plugin.get_local_collection_with_name( + collection_metadata["obj_container"].name + ) + objects = obj_container.all_objects + + self._remove(objects, obj_container) bpy.data.collections.remove(collection) return True -class CacheModelLoader(pype.hosts.blender.plugin.AssetLoader): +class CacheModelLoader(plugin.AssetLoader): """Load cache models. Stores the imported asset in a collection named after the asset. @@ -267,7 +280,7 @@ def process_asset( subset = context["subset"]["name"] # TODO (jasper): evaluate use of namespace which is 'alien' to Blender. lib_container = container_name = ( - pype.hosts.blender.plugin.asset_name(asset, subset, namespace) + plugin.asset_name(asset, subset, namespace) ) relative = bpy.context.preferences.filepaths.use_relative_paths diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 3e53ff03638..e09a9cb92f0 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -7,20 +7,14 @@ from avalon import api, blender import bpy -import pype.hosts.blender.plugin +import pype.hosts.blender.plugin as plugin -logger = logging.getLogger("pype").getChild("blender").getChild("load_model") - -class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): +class BlendRigLoader(plugin.AssetLoader): """Load rigs from a .blend file. Because they come from a .blend file we can simply link the collection that contains the model. There is no further need to 'containerise' it. - - Warning: - Loading the same asset more then once is not properly supported at the - moment. """ families = ["rig"] @@ -30,50 +24,54 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): icon = "code-fork" color = "orange" - def _remove(self, objects, lib_container): - + def _remove(self, objects, obj_container): for obj in objects: - if obj.type == 'ARMATURE': bpy.data.armatures.remove(obj.data) elif obj.type == 'MESH': bpy.data.meshes.remove(obj.data) - for child in bpy.data.collections[lib_container].children: + for child in obj_container.children: bpy.data.collections.remove(child) - bpy.data.collections.remove(bpy.data.collections[lib_container]) - - def _process(self, libpath, lib_container, container_name, action): + bpy.data.collections.remove(obj_container) + def _process( + self, libpath, lib_container, container_name, + action, parent_collection + ): relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( libpath, link=True, relative=relative ) as (_, data_to): data_to.collections = [lib_container] - scene = bpy.context.scene + parent = parent_collection - scene.collection.children.link(bpy.data.collections[lib_container]) + if parent is None: + parent = bpy.context.scene.collection - rig_container = scene.collection.children[lib_container].make_local() + parent.children.link(bpy.data.collections[lib_container]) + + rig_container = parent.children[lib_container].make_local() + rig_container.name = container_name meshes = [] armatures = [ - obj for obj in rig_container.objects if obj.type == 'ARMATURE'] - - objects_list = [] + obj for obj in rig_container.objects + if obj.type == 'ARMATURE' + ] for child in rig_container.children: - child.make_local() - meshes.extend( child.objects ) + plugin.prepare_data(child, container_name) + meshes.extend(child.objects) # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. for obj in meshes + armatures: - obj = obj.make_local() - obj.data.make_local() + plugin.prepare_data(obj, container_name) + plugin.prepare_data(obj.data, container_name) if not obj.get(blender.pipeline.AVALON_PROPERTY): obj[blender.pipeline.AVALON_PROPERTY] = dict() @@ -84,13 +82,11 @@ def _process(self, libpath, lib_container, container_name, action): if obj.type == 'ARMATURE' and action is not None: obj.animation_data.action = action - objects_list.append(obj) - rig_container.pop(blender.pipeline.AVALON_PROPERTY) bpy.ops.object.select_all(action='DESELECT') - return objects_list + return rig_container def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, @@ -107,9 +103,15 @@ def process_asset( libpath = self.fname asset = context["asset"]["name"] subset = context["subset"]["name"] - lib_container = pype.hosts.blender.plugin.asset_name(asset, subset) - container_name = pype.hosts.blender.plugin.asset_name( - asset, subset, namespace + lib_container = plugin.asset_name( + asset, subset + ) + unique_number = plugin.get_unique_number( + asset, subset + ) + namespace = namespace or f"{asset}_{unique_number}" + container_name = plugin.asset_name( + asset, subset, unique_number ) container = bpy.data.collections.new(lib_container) @@ -128,11 +130,13 @@ def process_asset( container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - objects_list = self._process( - libpath, lib_container, container_name, None) + obj_container = self._process( + libpath, lib_container, container_name, None, None) + + container_metadata["obj_container"] = obj_container # Save the list of objects in the metadata container - container_metadata["objects"] = objects_list + container_metadata["objects"] = obj_container.all_objects nodes = list(container.objects) nodes.append(container) @@ -151,15 +155,13 @@ def update(self, container: Dict, representation: Dict): Warning: No nested collections are supported at the moment! """ - collection = bpy.data.collections.get( container["objectName"] ) - libpath = Path(api.get_representation_path(representation)) extension = libpath.suffix.lower() - logger.info( + self.log.info( "Container: %s\nRepresentation: %s", pformat(container, indent=2), pformat(representation, indent=2), @@ -177,29 +179,35 @@ def update(self, container: Dict, representation: Dict): assert libpath.is_file(), ( f"The file doesn't exist: {libpath}" ) - assert extension in pype.hosts.blender.plugin.VALID_EXTENSIONS, ( + assert extension in plugin.VALID_EXTENSIONS, ( f"Unsupported file: {libpath}" ) collection_metadata = collection.get( blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] - objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] + obj_container = plugin.get_local_collection_with_name( + collection_metadata["obj_container"].name + ) + objects = obj_container.all_objects + + container_name = obj_container.name + normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) normalized_libpath = ( str(Path(bpy.path.abspath(str(libpath))).resolve()) ) - logger.debug( + self.log.debug( "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", normalized_collection_libpath, normalized_libpath, ) if normalized_collection_libpath == normalized_libpath: - logger.info("Library already loaded, not updating...") + self.log.info("Library already loaded, not updating...") return # Get the armature of the rig @@ -208,13 +216,16 @@ def update(self, container: Dict, representation: Dict): action = armatures[0].animation_data.action - self._remove(objects, lib_container) + parent = plugin.get_parent_collection(obj_container) + + self._remove(objects, obj_container) - objects_list = self._process( - str(libpath), lib_container, collection.name, action) + obj_container = self._process( + str(libpath), lib_container, container_name, action, parent) # Save the list of objects in the metadata container - collection_metadata["objects"] = objects_list + collection_metadata["obj_container"] = obj_container + collection_metadata["objects"] = obj_container.all_objects collection_metadata["libpath"] = str(libpath) collection_metadata["representation"] = str(representation["_id"]) @@ -245,10 +256,13 @@ def remove(self, container: Dict) -> bool: collection_metadata = collection.get( blender.pipeline.AVALON_PROPERTY) - objects = collection_metadata["objects"] - lib_container = collection_metadata["lib_container"] - self._remove(objects, lib_container) + obj_container = plugin.get_local_collection_with_name( + collection_metadata["obj_container"].name + ) + objects = obj_container.all_objects + + self._remove(objects, obj_container) bpy.data.collections.remove(collection)