mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
214 lines
7.1 KiB
Python
214 lines
7.1 KiB
Python
"""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")
|