Fix storing instance and context data correctly, fix removing instances + refactor instance nodes

This commit is contained in:
Roy Nieterau 2023-11-14 02:25:42 +01:00
parent 3d10bfdf09
commit 8575c4843c
18 changed files with 140 additions and 241 deletions

View file

@ -31,6 +31,14 @@ PREVIEW_COLLECTIONS: Dict = dict()
TIMER_INTERVAL: float = 0.01 if platform.system() == "Windows" else 0.1
def execute_function_in_main_thread(f):
"""Decorator to move a function call into main thread items"""
def wrapper(*args, **kwargs):
mti = MainThreadItem(f, *args, **kwargs)
execute_in_main_thread(mti)
return wrapper
class BlenderApplication(QtWidgets.QApplication):
_instance = None
blender_windows = {}

View file

@ -138,7 +138,10 @@ class BlenderHost(HostBase, IWorkfileHost, IPublishHost):
Returns:
dict: Context data stored using 'update_context_data'.
"""
return bpy.context.scene.openpype_context
property = bpy.context.scene.get(AVALON_PROPERTY)
if property:
return property.to_dict()
return {}
def update_context_data(self, data: dict, changes: dict):
"""Override abstract method from IPublishHost.
@ -149,7 +152,7 @@ class BlenderHost(HostBase, IWorkfileHost, IPublishHost):
changes (dict): Only data that has been changed. Each value has
tuple with '(<old>, <new>)' value.
"""
bpy.context.scene.openpype_context.update(data)
bpy.context.scene[AVALON_PROPERTY] = data
def pype_excepthook_handler(*args):

View file

@ -12,6 +12,8 @@ from openpype.pipeline import (
LoaderPlugin,
get_current_task_name,
)
from openpype.lib import BoolDef
from .pipeline import (
AVALON_CONTAINERS,
AVALON_INSTANCES,
@ -149,6 +151,8 @@ class BaseCreator(Creator):
"""Base class for Blender Creator plug-ins."""
defaults = ['Main']
create_as_asset_group = False
@staticmethod
def cache_subsets(shared_data):
"""Cache instances for Creators shared data.
@ -190,12 +194,12 @@ class BaseCreator(Creator):
creator_id = avalon_prop.get('creator_identifier')
if creator_id:
# Creator instance
cache.setdefault(creator_id, []).append(avalon_prop)
cache.setdefault(creator_id, []).append(obj_or_col)
else:
family = avalon_prop.get('family')
if family:
# Legacy creator instance
cache_legacy.setdefault(family, []).append(avalon_prop)
cache_legacy.setdefault(family, []).append(obj_or_col)
shared_data["blender_cached_subsets"] = cache
shared_data["blender_cached_legacy_subsets"] = cache_legacy
@ -220,27 +224,29 @@ class BaseCreator(Creator):
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
bpy.context.scene.collection.children.link(instances)
# Create instance collection
collection = bpy.data.collections.new(
name=asset_name(instance_data["asset"], subset_name)
# Create asset group
name = asset_name(instance_data["asset"], subset_name)
if self.create_as_asset_group:
# Create instance as empty
instance_node = bpy.data.objects.new(name=name, object_data=None)
instance_node.empty_display_type = 'SINGLE_ARROW'
instances.objects.link(instance_node)
else:
# Create instance collection
instance_node = bpy.data.collections.new(name=name)
instances.children.link(instance_node)
self.set_instance_data(subset_name, instance_data)
instance = CreatedInstance(
self.family, subset_name, instance_data, self
)
instances.children.link(collection)
instance.transient_data["instance_node"] = instance_node
self._add_instance_to_context(instance)
collection[AVALON_PROPERTY] = instance_node = {
"name": collection.name,
}
imprint(instance_node, instance_data)
self.set_instance_data(subset_name, instance_data, instance_node)
self._add_instance_to_context(
CreatedInstance(
self.family, subset_name, instance_data, self
)
)
imprint(collection, instance_data)
return collection
return instance_node
def collect_instances(self):
"""Override abstract method from BaseCreator.
@ -257,12 +263,14 @@ class BaseCreator(Creator):
return
# Process only instances that were created by this creator
for instance_data in cached_subsets.get(self.identifier, []):
for instance_node in cached_subsets.get(self.identifier, []):
property = instance_node.get(AVALON_PROPERTY)
# Create instance object from existing data
instance = CreatedInstance.from_existing(
instance_data=instance_data.to_dict(),
instance_data=property.to_dict(),
creator=self
)
instance.transient_data["instance_node"] = instance_node
# Add instance to create context
self._add_instance_to_context(instance)
@ -276,41 +284,32 @@ class BaseCreator(Creator):
and their changes, as a list of tuples."""
for created_instance, _changes in update_list:
data = created_instance.data_to_store()
imprint(data.get("instance_node", {}), data)
node = created_instance.transient_data["instance_node"]
if node:
imprint(node, data)
def remove_instances(self, instances: List[CreatedInstance]):
"""Override abstract method from BaseCreator.
Method called when instances are removed.
Args:
instance(List[CreatedInstance]): Instance objects to remove.
"""
for instance in instances:
outliner_entity = instance.data.get("instance_node", {}).get(
"datablock"
)
if not outliner_entity:
continue
node = instance.transient_data["instance_node"]
if isinstance(outliner_entity, bpy.types.Collection):
for children in outliner_entity.children_recursive:
if isinstance(node, bpy.types.Collection):
for children in node.children_recursive:
if isinstance(children, bpy.types.Collection):
bpy.data.collections.remove(children)
else:
bpy.data.objects.remove(children)
bpy.data.collections.remove(outliner_entity)
elif isinstance(outliner_entity, bpy.types.Object):
bpy.data.objects.remove(outliner_entity)
bpy.data.collections.remove(node)
elif isinstance(node, bpy.types.Object):
bpy.data.objects.remove(node)
self._remove_instance_from_context(instance)
def set_instance_data(
self,
subset_name: str,
instance_data: dict,
instance_node: bpy.types.ID,
instance_data: dict
):
"""Fill instance data with required items.
@ -329,10 +328,16 @@ class BaseCreator(Creator):
"label": subset_name,
"task": get_current_task_name(),
"subset": subset_name,
"instance_node": instance_node,
}
)
def get_pre_create_attr_defs(self):
return [
BoolDef("use_selection",
label="Use selection",
default=True)
]
class Loader(LoaderPlugin):
"""Base class for Loader plug-ins."""

View file

@ -2,11 +2,10 @@
import bpy
from openpype.hosts.blender.api.plugin import BaseCreator, asset_name
from openpype.hosts.blender.api import lib
from openpype.hosts.blender.api import lib, plugin
class CreateAction(BaseCreator):
class CreateAction(plugin.BaseCreator):
"""Action output for character rigs."""
identifier = "io.openpype.creators.blender.action"
@ -24,9 +23,9 @@ class CreateAction(BaseCreator):
)
# Get instance name
name = asset_name(instance_data["asset"], subset_name)
name = plugin.asset_name(instance_data["asset"], subset_name)
if pre_create_data.get("useSelection"):
if pre_create_data.get("use_selection"):
for obj in lib.get_selection():
if (obj.animation_data is not None
and obj.animation_data.action is not None):

View file

@ -21,7 +21,7 @@ class CreateAnimation(plugin.BaseCreator):
subset_name, instance_data, pre_create_data
)
if pre_create_data.get("useSelection"):
if pre_create_data.get("use_selection"):
selected = lib.get_selection()
for obj in selected:
collection.objects.link(obj)

View file

@ -23,42 +23,17 @@ class CreateBlendScene(plugin.Creator):
def create(
self, subset_name: str, instance_data: dict, pre_create_data: dict
):
"""Run the creator on Blender main thread."""
mti = ops.MainThreadItem(
self._process, subset_name, instance_data, pre_create_data
)
ops.execute_in_main_thread(mti)
def _process(
self, subset_name: str, instance_data: dict, pre_create_data: dict
):
# Get Instance Container or create it if it does not exist
instances = bpy.data.collections.get(AVALON_INSTANCES)
if not instances:
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
bpy.context.scene.collection.children.link(instances)
instance_node = super().create(subset_name,
instance_data,
pre_create_data)
# Create instance object
asset = instance_data.get("asset")
name = plugin.asset_name(asset, subset_name)
# Create the new asset group as collection
asset_group = bpy.data.collections.new(name=name)
instances.children.link(asset_group)
asset_group[AVALON_PROPERTY] = instance_node = {
"name": asset_group.name
}
self.set_instance_data(subset_name, instance_data, instance_node)
lib.imprint(asset_group, instance_data)
if (self.options or {}).get("useSelection"):
if pre_create_data.get("use_selection"):
selection = lib.get_selection(include_collections=True)
for data in selection:
if isinstance(data, bpy.types.Collection):
asset_group.children.link(data)
instance_node.children.link(data)
elif isinstance(data, bpy.types.Object):
asset_group.objects.link(data)
instance_node.objects.link(data)
return asset_group
return instance_node

View file

@ -2,7 +2,7 @@
import bpy
from openpype.pipeline import get_current_task_name, CreatedInstance
from openpype.pipeline import CreatedInstance
from openpype.hosts.blender.api import plugin, lib, ops
from openpype.hosts.blender.api.pipeline import (
AVALON_INSTANCES,
@ -19,43 +19,19 @@ class CreateCamera(plugin.BaseCreator):
family = "camera"
icon = "video-camera"
create_as_asset_group = True
@ops.execute_function_in_main_thread
def create(
self, subset_name: str, instance_data: dict, pre_create_data: dict
):
"""Run the creator on Blender main thread."""
self._add_instance_to_context(
CreatedInstance(self.family, subset_name, instance_data, self)
)
mti = ops.MainThreadItem(
self._process, subset_name, instance_data, pre_create_data
)
ops.execute_in_main_thread(mti)
asset_group = super().create(subset_name,
instance_data,
pre_create_data)
def _process(
self, subset_name: str, instance_data: dict, pre_create_data: dict
):
# Get Instance Container or create it if it does not exist
instances = bpy.data.collections.get(AVALON_INSTANCES)
if not instances:
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
bpy.context.scene.collection.children.link(instances)
# Create instance object
name = plugin.asset_name(instance_data["asset"], subset_name)
asset_group = bpy.data.objects.new(name=name, object_data=None)
asset_group.empty_display_type = 'SINGLE_ARROW'
instances.objects.link(asset_group)
asset_group[AVALON_PROPERTY] = instance_node = {
"name": asset_group.name,
}
self.set_instance_data(subset_name, instance_data, instance_node)
lib.imprint(asset_group, instance_data)
if pre_create_data.get("useSelection"):
if pre_create_data.get("use_selection"):
bpy.context.view_layer.objects.active = asset_group
selected = lib.get_selection()
for obj in selected:
@ -67,6 +43,7 @@ class CreateCamera(plugin.BaseCreator):
camera = bpy.data.cameras.new(subset_name)
camera_obj = bpy.data.objects.new(subset_name, camera)
instances = bpy.data.collections.get(AVALON_INSTANCES)
instances.objects.link(camera_obj)
camera_obj.select_set(True)

View file

@ -19,43 +19,18 @@ class CreateLayout(plugin.BaseCreator):
family = "layout"
icon = "cubes"
create_as_asset_group = True
def create(
self, subset_name: str, instance_data: dict, pre_create_data: dict
):
"""Run the creator on Blender main thread."""
self._add_instance_to_context(
CreatedInstance(self.family, subset_name, instance_data, self)
)
mti = ops.MainThreadItem(
self._process, subset_name, instance_data, pre_create_data
)
ops.execute_in_main_thread(mti)
def _process(
self, subset_name: str, instance_data: dict, pre_create_data: dict
):
# Get Instance Container or create it if it does not exist
instances = bpy.data.collections.get(AVALON_INSTANCES)
if not instances:
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
bpy.context.scene.collection.children.link(instances)
# Create instance object
name = plugin.asset_name(instance_data["asset"], subset_name)
asset_group = bpy.data.objects.new(name=name, object_data=None)
asset_group.empty_display_type = 'SINGLE_ARROW'
instances.objects.link(asset_group)
asset_group[AVALON_PROPERTY] = instance_node = {
"name": asset_group.name,
}
self.set_instance_data(subset_name, instance_data, instance_node)
lib.imprint(asset_group, instance_data)
asset_group = super().create(subset_name,
instance_data,
pre_create_data)
# Add selected objects to instance
if pre_create_data.get("useSelection"):
if pre_create_data.get("use_selection"):
bpy.context.view_layer.objects.active = asset_group
selected = lib.get_selection()
for obj in selected:

View file

@ -19,48 +19,24 @@ class CreateModel(plugin.BaseCreator):
family = "model"
icon = "cube"
create_as_asset_group = True
@ops.execute_function_in_main_thread
def create(
self, subset_name: str, instance_data: dict, pre_create_data: dict
):
"""Run the creator on Blender main thread."""
self._add_instance_to_context(
CreatedInstance(self.family, subset_name, instance_data, self)
)
mti = ops.MainThreadItem(
self._process, subset_name, instance_data, pre_create_data
)
ops.execute_in_main_thread(mti)
def _process(
self, subset_name: str, instance_data: dict, pre_create_data: dict
):
# Get Instance Container or create it if it does not exist
instances = bpy.data.collections.get(AVALON_INSTANCES)
if not instances:
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
bpy.context.scene.collection.children.link(instances)
# Create instance object
name = plugin.asset_name(instance_data["asset"], subset_name)
asset_group = bpy.data.objects.new(name=name, object_data=None)
asset_group.empty_display_type = 'SINGLE_ARROW'
instances.objects.link(asset_group)
asset_group[AVALON_PROPERTY] = instance_node = {
"name": asset_group.name,
}
self.set_instance_data(subset_name, instance_data, instance_node)
lib.imprint(asset_group, instance_data)
asset_group = super().create(subset_name,
instance_data,
pre_create_data)
# Add selected objects to instance
if pre_create_data.get("useSelection"):
if pre_create_data.get("use_selection"):
bpy.context.view_layer.objects.active = asset_group
selected = lib.get_selection()
for obj in selected:
obj.select_set(True)
selected.append(asset_group)
bpy.ops.object.parent_set(keep_transform=True)
return asset_group

View file

@ -20,7 +20,7 @@ class CreatePointcache(plugin.BaseCreator):
subset_name, instance_data, pre_create_data
)
if pre_create_data.get("useSelection"):
if pre_create_data.get("use_selection"):
objects = lib.get_selection()
for obj in objects:
collection.objects.link(obj)

View file

@ -21,11 +21,12 @@ class CreateReview(plugin.BaseCreator):
subset_name, instance_data, pre_create_data
)
if pre_create_data.get("useSelection"):
if pre_create_data.get("use_selection"):
selected = lib.get_selection()
for obj in selected:
collection.objects.link(obj)
elif pre_create_data.get("asset_group"):
# TODO: What is the intended behavior for this?
obj = (self.options or {}).get("asset_group")
collection.objects.link(obj)

View file

@ -19,43 +19,18 @@ class CreateRig(plugin.BaseCreator):
family = "rig"
icon = "wheelchair"
create_as_asset_group = True
@ops.execute_function_in_main_thread
def create(
self, subset_name: str, instance_data: dict, pre_create_data: dict
):
"""Run the creator on Blender main thread."""
self._add_instance_to_context(
CreatedInstance(self.family, subset_name, instance_data, self)
)
mti = ops.MainThreadItem(
self._process, subset_name, instance_data, pre_create_data
)
ops.execute_in_main_thread(mti)
def _process(
self, subset_name: str, instance_data: dict, pre_create_data: dict
):
# Get Instance Container or create it if it does not exist
instances = bpy.data.collections.get(AVALON_INSTANCES)
if not instances:
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
bpy.context.scene.collection.children.link(instances)
# Create instance object
name = plugin.asset_name(instance_data["asset"], subset_name)
asset_group = bpy.data.objects.new(name=name, object_data=None)
asset_group.empty_display_type = 'SINGLE_ARROW'
instances.objects.link(asset_group)
asset_group[AVALON_PROPERTY] = instance_node = {
"name": asset_group.name,
}
self.set_instance_data(subset_name, instance_data, instance_node)
lib.imprint(asset_group, instance_data)
asset_group = super().create(subset_name,
instance_data,
pre_create_data)
# Add selected objects to instance
if pre_create_data.get("useSelection"):
if pre_create_data.get("use_selection"):
bpy.context.view_layer.objects.active = asset_group
selected = lib.get_selection()
for obj in selected:

View file

@ -3,8 +3,10 @@ import bpy
from openpype.pipeline import CreatedInstance, AutoCreator
from openpype.client import get_asset_by_name
from openpype.hosts.blender.api.plugin import BaseCreator
from openpype.hosts.blender.api.lib import imprint
from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY
from openpype.hosts.blender.api.pipeline import (
AVALON_PROPERTY,
AVALON_CONTAINERS
)
class CreateWorkfile(BaseCreator, AutoCreator):
@ -53,6 +55,8 @@ class CreateWorkfile(BaseCreator, AutoCreator):
current_instance = CreatedInstance(
self.family, subset_name, data, self
)
instance_node = bpy.data.collections.get(AVALON_CONTAINERS, {})
current_instance.transient_data["instance_node"] = instance_node
self._add_instance_to_context(current_instance)
elif (
current_instance["asset"] != asset_name
@ -73,28 +77,30 @@ class CreateWorkfile(BaseCreator, AutoCreator):
)
def collect_instances(self):
"""Collect workfile instances."""
self.cache_subsets(self.collection_shared_data)
cached_subsets = self.collection_shared_data["blender_cached_subsets"]
for node in cached_subsets.get(self.identifier, []):
created_instance = CreatedInstance.from_existing(
self.read_instance_node(node), self
)
self._add_instance_to_context(created_instance)
def update_instances(self, update_list):
"""Update workfile instances."""
for created_inst, _changes in update_list:
data = created_inst.data_to_store()
node = data.get("instance_node")
if not node:
task_name = self.create_context.get_current_task_name()
print("Collecting!")
instance_node = bpy.data.collections.get(AVALON_CONTAINERS)
if not instance_node:
return
print(instance_node)
property = instance_node.get(AVALON_PROPERTY)
if not property:
return
print(property)
bpy.context.scene[AVALON_PROPERTY] = node = {
"name": f"workfile{task_name}"
}
# Create instance object from existing data
instance = CreatedInstance.from_existing(
instance_data=property.to_dict(),
creator=self
)
instance.transient_data["instance_node"] = instance_node
created_inst["instance_node"] = node
data = created_inst.data_to_store()
# Add instance to create context
self._add_instance_to_context(instance)
imprint(node, data)
def remove_instances(self, instances):
for instance in instances:
node = instance.transient_data["instance_node"]
del node[AVALON_PROPERTY]
self._remove_instance_from_context(instance)

View file

@ -60,6 +60,7 @@ class BlendLoader(plugin.AssetLoader):
for rig in rigs:
creator_plugin = get_legacy_creator_by_name("CreateAnimation")
# TODO: Refactor legacy create usage to new style creators
legacy_create(
creator_plugin,
name=rig.name.split(':')[-1] + "_animation",

View file

@ -123,6 +123,7 @@ class JsonLayoutLoader(plugin.AssetLoader):
# raise ValueError("Creator plugin \"CreateCamera\" was "
# "not found.")
# TODO: Refactor legacy create usage to new style creators
# legacy_create(
# creator_plugin,
# name="camera",

View file

@ -73,9 +73,8 @@ class CollectBlenderRender(pyblish.api.InstancePlugin):
def process(self, instance):
context = instance.context
render_data = bpy.data.collections[
instance.data["instance_node"]["name"]
].get("render_data")
instance_node = instance.data["transientData"]["instance_node"]
render_data = instance_node.get("render_data")
assert render_data, "No render data found."

View file

@ -16,9 +16,7 @@ class CollectReview(pyblish.api.InstancePlugin):
self.log.debug(f"instance: {instance}")
datablock = bpy.data.collections.get(
instance.data.get("instance_node", {}).get("name", "")
)
datablock = instance.data["transientData"]["instance_node"]
# get cameras
cameras = [

View file

@ -13,7 +13,7 @@ class ValidateInstanceEmpty(pyblish.api.InstancePlugin):
optional = False
def process(self, instance):
asset_group = instance.data["instance_node"]
asset_group = instance.data["transientData"]["instance_node"]
if isinstance(asset_group, bpy.types.Collection):
if not (asset_group.objects or asset_group.children):