"""Shared functionality for pipeline plugins for Blender.""" from pathlib import Path from typing import Dict, List, Optional import bpy from avalon import api VALID_EXTENSIONS = [".blend"] def asset_name( asset: str, subset: str, namespace: Optional[str] = None ) -> str: """Return a consistent name for an asset.""" name = f"{asset}" if namespace: 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 local_data = data.make_local() local_data.name = f"{name}:{container_name}" return local_data 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 parameter, it is set as selected and active. """ if not isinstance(selected, list): selected = [selected] for win in bpy.context.window_manager.windows: for area in win.screen.areas: if area.type == 'VIEW_3D': for region in area.regions: if region.type == 'WINDOW': override_context = { 'window': win, 'screen': win.screen, 'area': area, 'region': region, 'scene': bpy.context.scene, 'active_object': active, 'selected_objects': selected } return override_context 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 This will implement the basic logic for linking/appending assets into another Blender scene. The `update` method should be implemented by a sub-class, because it's different for different types (e.g. model, rig, animation, etc.). """ @staticmethod def _get_instance_empty(instance_name: str, nodes: List) -> Optional[bpy.types.Object]: """Get the 'instance empty' that holds the collection instance.""" for node in nodes: if not isinstance(node, bpy.types.Object): continue if (node.type == 'EMPTY' and node.instance_type == 'COLLECTION' and node.instance_collection and node.name == instance_name): return node return None @staticmethod def _get_instance_collection(instance_name: str, nodes: List) -> Optional[bpy.types.Collection]: """Get the 'instance collection' (container) for this asset.""" for node in nodes: if not isinstance(node, bpy.types.Collection): continue if node.name == instance_name: return node return None @staticmethod def _get_library_from_container(container: bpy.types.Collection) -> bpy.types.Library: """Find the library file from the container. It traverses the objects from this collection, checks if there is only 1 library from which the objects come from and returns the library. Warning: No nested collections are supported at the moment! """ assert not container.children, "Nested collections are not supported." assert container.objects, "The collection doesn't contain any objects." libraries = set() for obj in container.objects: assert obj.library, f"'{obj.name}' is not linked." libraries.add(obj.library) assert len( libraries) == 1, "'{container.name}' contains objects from more then 1 library." return list(libraries)[0] def process_asset(self, context: dict, name: str, namespace: Optional[str] = None, options: Optional[Dict] = None): """Must be implemented by a sub-class""" raise NotImplementedError("Must be implemented by a sub-class") def load(self, context: dict, name: Optional[str] = None, namespace: Optional[str] = None, options: Optional[Dict] = None) -> Optional[bpy.types.Collection]: """Load asset via database Arguments: context: Full parenthood of representation to load name: Use pre-defined name namespace: Use pre-defined namespace options: Additional settings dictionary """ # TODO (jasper): make it possible to add the asset several times by # just re-using the collection assert Path(self.fname).exists(), f"{self.fname} doesn't exist." self.process_asset( context=context, name=name, namespace=namespace, options=options, ) # Only containerise if anything was loaded by the Loader. nodes = self[:] if not nodes: return None # Only containerise if it's not already a collection from a .blend file. representation = context["representation"]["name"] if representation != "blend": from avalon.blender.pipeline import containerise return containerise( name=name, namespace=namespace, nodes=nodes, context=context, loader=self.__class__.__name__, ) asset = context["asset"]["name"] subset = context["subset"]["name"] instance_name = asset_name(asset, subset, namespace) return self._get_instance_collection(instance_name, nodes) def update(self, container: Dict, representation: Dict): """Must be implemented by a sub-class""" raise NotImplementedError("Must be implemented by a sub-class") def remove(self, container: Dict) -> bool: """Must be implemented by a sub-class""" raise NotImplementedError("Must be implemented by a sub-class")