From c60bd1cb2df44735ed1ce719f88449928f8d4e4e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 17:56:29 +0100 Subject: [PATCH 1/3] Fusion: Add Validate Instance in Context validator --- client/ayon_core/hosts/fusion/api/action.py | 52 ++++++++++++ .../publish/validate_instance_in_context.py | 80 +++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 client/ayon_core/hosts/fusion/plugins/publish/validate_instance_in_context.py diff --git a/client/ayon_core/hosts/fusion/api/action.py b/client/ayon_core/hosts/fusion/api/action.py index 1643f1ce03..a0c6aafcb5 100644 --- a/client/ayon_core/hosts/fusion/api/action.py +++ b/client/ayon_core/hosts/fusion/api/action.py @@ -58,3 +58,55 @@ class SelectInvalidAction(pyblish.api.Action): self.log.info( "Selecting invalid tools: %s" % ", ".join(sorted(names)) ) + + +class SelectToolAction(pyblish.api.Action): + """Select invalid output tool in Fusion when plug-in failed. + + """ + + label = "Select saver" + on = "failed" # This action is only available on a failed plug-in + icon = "search" # Icon from Awesome Icon + + def process(self, context, plugin): + errored_instances = get_errored_instances_from_context( + context, + plugin=plugin, + ) + + # Get the invalid nodes for the plug-ins + self.log.info("Finding invalid nodes..") + tools = [] + for instance in errored_instances: + + tool = instance.data.get("tool") + if tool is not None: + tools.append(tool) + else: + self.log.warning( + "Plug-in returned to be invalid, " + f"but has no saver for instance {instance.name}." + ) + + if not tools: + # Assume relevant comp is current comp and clear selection + self.log.info("No invalid tools found.") + comp = get_current_comp() + flow = comp.CurrentFrame.FlowView + flow.Select() # No args equals clearing selection + return + + # Assume a single comp + first_tool = tools[0] + comp = first_tool.Comp() + flow = comp.CurrentFrame.FlowView + flow.Select() # No args equals clearing selection + names = set() + for tool in tools: + flow.Select(tool, True) + comp.SetActiveTool(tool) + names.add(tool.Name) + self.log.info( + "Selecting invalid tools: %s" % ", ".join(sorted(names)) + ) diff --git a/client/ayon_core/hosts/fusion/plugins/publish/validate_instance_in_context.py b/client/ayon_core/hosts/fusion/plugins/publish/validate_instance_in_context.py new file mode 100644 index 0000000000..3aa6fb452f --- /dev/null +++ b/client/ayon_core/hosts/fusion/plugins/publish/validate_instance_in_context.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +"""Validate if instance context is the same as publish context.""" + +import pyblish.api +from ayon_core.hosts.fusion.api.action import SelectToolAction +from ayon_core.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishValidationError, + OptionalPyblishPluginMixin +) + + +class ValidateInstanceInContextFusion(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validator to check if instance context matches context of publish. + + When working in per-shot style you always publish data in context of + current asset (shot). This validator checks if this is so. It is optional + so it can be disabled when needed. + """ + # Similar to maya and houdini-equivalent `ValidateInstanceInContext` + + order = ValidateContentsOrder + label = "Instance in same Context" + optional = True + hosts = ["fusion"] + actions = [SelectToolAction, RepairAction] + + def process(self, instance): + if not self.is_active(instance.data): + return + + instance_context = self.get_context(instance.data) + context = self.get_context(instance.context.data) + if instance_context != context: + context_label = "{} > {}".format(*context) + instance_label = "{} > {}".format(*instance_context) + + raise PublishValidationError( + message=( + "Instance '{}' publishes to different asset than current " + "context: {}. Current context: {}".format( + instance.name, instance_label, context_label + ) + ), + description=( + "## Publishing to a different asset\n" + "There are publish instances present which are publishing " + "into a different asset than your current context.\n\n" + "Usually this is not what you want but there can be cases " + "where you might want to publish into another asset or " + "shot. If that's the case you can disable the validation " + "on the instance to ignore it." + ) + ) + + @classmethod + def repair(cls, instance): + + create_context = instance.context.data["create_context"] + instance_id = instance.data.get("instance_id") + created_instance = create_context.get_instance_by_id( + instance_id + ) + if created_instance is None: + raise RuntimeError( + f"No CreatedInstances found with id '{instance_id} " + f"in {create_context.instances_by_id}" + ) + + context_asset, context_task = cls.get_context(instance.context.data) + created_instance["folderPath"] = context_asset + created_instance["task"] = context_task + create_context.save_changes() + + @staticmethod + def get_context(data): + """Return asset, task from publishing context data""" + return data["folderPath"], data["task"] From 7d9ff383096a262bb03b66009802945c974dad8a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 17:57:02 +0100 Subject: [PATCH 2/3] Correctly preserve the instance's task instead of forcing current context task --- client/ayon_core/hosts/fusion/plugins/publish/collect_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/fusion/plugins/publish/collect_render.py b/client/ayon_core/hosts/fusion/plugins/publish/collect_render.py index 36102d02cb..b1ecce728b 100644 --- a/client/ayon_core/hosts/fusion/plugins/publish/collect_render.py +++ b/client/ayon_core/hosts/fusion/plugins/publish/collect_render.py @@ -53,7 +53,7 @@ class CollectFusionRender( if product_type not in ["render", "image"]: continue - task_name = context.data["task"] + task_name = inst.data["task"] tool = inst.data["transientData"]["tool"] instance_families = inst.data.get("families", []) From 4c51f3a560109fa75c50f4104e609f93b4c8f28b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 17:44:31 +0100 Subject: [PATCH 3/3] Implement fix for losing instance id and disconnected logs in publisher UI --- .../fusion/plugins/publish/collect_render.py | 7 +++++++ .../publish/abstract_collect_render.py | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/client/ayon_core/hosts/fusion/plugins/publish/collect_render.py b/client/ayon_core/hosts/fusion/plugins/publish/collect_render.py index b1ecce728b..3f5e2837bc 100644 --- a/client/ayon_core/hosts/fusion/plugins/publish/collect_render.py +++ b/client/ayon_core/hosts/fusion/plugins/publish/collect_render.py @@ -119,6 +119,13 @@ class CollectFusionRender( instances.append(instance) instances_to_remove.append(inst) + # TODO: Avoid this transfer instance id hack + # pass on the `id` of the original instance so any artist + # facing logs transfer as if they were made on the new instance + # instead, see `AbstractCollectRender.process()` + instance.id = inst.id + instance.instance_id = inst.data.get("instance_id") + for instance in instances_to_remove: context.remove(instance) diff --git a/client/ayon_core/pipeline/publish/abstract_collect_render.py b/client/ayon_core/pipeline/publish/abstract_collect_render.py index 745632ca0a..8b98cb678e 100644 --- a/client/ayon_core/pipeline/publish/abstract_collect_render.py +++ b/client/ayon_core/pipeline/publish/abstract_collect_render.py @@ -215,6 +215,25 @@ class AbstractCollectRender(pyblish.api.ContextPlugin): render_instance_dict = attr.asdict(render_instance) instance = context.create_instance(render_instance.name) + + # TODO: Avoid this transfer instance id hack + # Transfer the id from another instance, e.g. when the render + # instance is intended to "replace" an existing instance like + # fusion does in `CollectRender`. Without matching the ids any + # logs produced for the instance prior to the "replacement" will + # not show artist-facing logs in reports + transfer_id = getattr(render_instance, "id") + if transfer_id: + instance._id = transfer_id + # The `instance_id` data may be overridden on the Creator + # to e.g. maybe make unique by node name instead of uuid, + # like in Maya, Fusion, Houdini integration. + # This transfers that unique (named) instance id. + # This transfer logic is currently (only?) used in Fusion. + transfer_instance_id = getattr(render_instance, "instance_id") + if transfer_instance_id: + instance.data["instance_id"] = transfer_instance_id + instance.data["label"] = render_instance.label instance.data.update(render_instance_dict) instance.data.update(data)