From 2ace936a951c6562145b4dd359e104eca629bd05 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 23:32:13 +0200 Subject: [PATCH 01/30] Draft to refactor fusion to new publisher and use as FusionHost --- openpype/hosts/fusion/api/__init__.py | 27 +-- openpype/hosts/fusion/api/menu.py | 2 +- openpype/hosts/fusion/api/pipeline.py | 174 ++++++++++++------ openpype/hosts/fusion/api/workio.py | 45 ----- .../deploy/MenuScripts/openpype_menu.py | 4 +- .../fusion/plugins/create/create_exr_saver.py | 44 +++-- 6 files changed, 160 insertions(+), 136 deletions(-) delete mode 100644 openpype/hosts/fusion/api/workio.py diff --git a/openpype/hosts/fusion/api/__init__.py b/openpype/hosts/fusion/api/__init__.py index ed70dbca50..495fe286d5 100644 --- a/openpype/hosts/fusion/api/__init__.py +++ b/openpype/hosts/fusion/api/__init__.py @@ -1,20 +1,11 @@ from .pipeline import ( - install, - uninstall, - + FusionHost, ls, imprint_container, - parse_container -) - -from .workio import ( - open_file, - save_file, - current_file, - has_unsaved_changes, - file_extensions, - work_root + parse_container, + list_instances, + remove_instance ) from .lib import ( @@ -30,21 +21,11 @@ from .menu import launch_openpype_menu __all__ = [ # pipeline - "install", - "uninstall", "ls", "imprint_container", "parse_container", - # workio - "open_file", - "save_file", - "current_file", - "has_unsaved_changes", - "file_extensions", - "work_root", - # lib "maintained_selection", "update_frame_range", diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 7a6293807f..4e415cafba 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -144,7 +144,7 @@ class OpenPypeMenu(QtWidgets.QWidget): host_tools.show_creator() def on_publish_clicked(self): - host_tools.show_publish() + host_tools.show_publisher() def on_load_clicked(self): host_tools.show_loader(use_context=True) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index c92d072ef7..b73759fee0 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -3,6 +3,7 @@ Basic avalon integration """ import os import logging +import contextlib import pyblish.api @@ -14,15 +15,14 @@ from openpype.pipeline import ( register_loader_plugin_path, register_creator_plugin_path, register_inventory_action_path, - deregister_loader_plugin_path, - deregister_creator_plugin_path, - deregister_inventory_action_path, AVALON_CONTAINER_ID, ) from openpype.pipeline.load import any_outdated_containers from openpype.hosts.fusion import FUSION_HOST_DIR +from openpype.host import HostBase, IWorkfileHost, ILoadHost, INewPublisher from openpype.tools.utils import host_tools + from .lib import ( get_current_comp, comp_lock_and_undo_chunk, @@ -47,71 +47,99 @@ class CompLogHandler(logging.Handler): comp.Print(entry) -def install(): - """Install fusion-specific functionality of OpenPype. +class FusionHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): + name = "fusion" - This is where you install menus and register families, data - and loaders into fusion. + def install(self): + """Install fusion-specific functionality of OpenPype. - It is called automatically when installing via - `openpype.pipeline.install_host(openpype.hosts.fusion.api)` + This is where you install menus and register families, data + and loaders into fusion. - See the Maya equivalent for inspiration on how to implement this. + It is called automatically when installing via + `openpype.pipeline.install_host(openpype.hosts.fusion.api)` - """ - # Remove all handlers associated with the root logger object, because - # that one always logs as "warnings" incorrectly. - for handler in logging.root.handlers[:]: - logging.root.removeHandler(handler) + See the Maya equivalent for inspiration on how to implement this. - # Attach default logging handler that prints to active comp - logger = logging.getLogger() - formatter = logging.Formatter(fmt="%(message)s\n") - handler = CompLogHandler() - handler.setFormatter(formatter) - logger.addHandler(handler) - logger.setLevel(logging.DEBUG) + """ + # Remove all handlers associated with the root logger object, because + # that one always logs as "warnings" incorrectly. + for handler in logging.root.handlers[:]: + logging.root.removeHandler(handler) - pyblish.api.register_host("fusion") - pyblish.api.register_plugin_path(PUBLISH_PATH) - log.info("Registering Fusion plug-ins..") + # Attach default logging handler that prints to active comp + logger = logging.getLogger() + formatter = logging.Formatter(fmt="%(message)s\n") + handler = CompLogHandler() + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.setLevel(logging.DEBUG) - register_loader_plugin_path(LOAD_PATH) - register_creator_plugin_path(CREATE_PATH) - register_inventory_action_path(INVENTORY_PATH) + pyblish.api.register_host("fusion") + pyblish.api.register_plugin_path(PUBLISH_PATH) + log.info("Registering Fusion plug-ins..") - pyblish.api.register_callback( - "instanceToggled", on_pyblish_instance_toggled - ) + register_loader_plugin_path(LOAD_PATH) + register_creator_plugin_path(CREATE_PATH) + register_inventory_action_path(INVENTORY_PATH) - # Fusion integration currently does not attach to direct callbacks of - # the application. So we use workfile callbacks to allow similar behavior - # on save and open - register_event_callback("workfile.open.after", on_after_open) + pyblish.api.register_callback("instanceToggled", + on_pyblish_instance_toggled) + # Fusion integration currently does not attach to direct callbacks of + # the application. So we use workfile callbacks to allow similar + # behavior on save and open + register_event_callback("workfile.open.after", on_after_open) -def uninstall(): - """Uninstall all that was installed + # region workfile io api + def has_unsaved_changes(self): + comp = get_current_comp() + return comp.GetAttrs()["COMPB_Modified"] - This is where you undo everything that was done in `install()`. - That means, removing menus, deregistering families and data - and everything. It should be as though `install()` was never run, - because odds are calling this function means the user is interested - in re-installing shortly afterwards. If, for example, he has been - modifying the menu or registered families. + def get_workfile_extensions(self): + return [".comp"] - """ - pyblish.api.deregister_host("fusion") - pyblish.api.deregister_plugin_path(PUBLISH_PATH) - log.info("Deregistering Fusion plug-ins..") + def save_workfile(self, dst_path=None): + comp = get_current_comp() + comp.Save(dst_path) - deregister_loader_plugin_path(LOAD_PATH) - deregister_creator_plugin_path(CREATE_PATH) - deregister_inventory_action_path(INVENTORY_PATH) + def open_workfile(self, filepath): + # Hack to get fusion, see + # openpype.hosts.fusion.api.pipeline.get_current_comp() + fusion = getattr(sys.modules["__main__"], "fusion", None) - pyblish.api.deregister_callback( - "instanceToggled", on_pyblish_instance_toggled - ) + return fusion.LoadComp(filepath) + + def get_current_workfile(self): + comp = get_current_comp() + current_filepath = comp.GetAttrs()["COMPS_FileName"] + if not current_filepath: + return None + + return current_filepath + + def work_root(self, session): + work_dir = session["AVALON_WORKDIR"] + scene_dir = session.get("AVALON_SCENEDIR") + if scene_dir: + return os.path.join(work_dir, scene_dir) + else: + return work_dir + # endregion + + @contextlib.contextmanager + def maintained_selection(self): + from .lib import maintained_selection + return maintained_selection() + + def get_containers(self): + return ls() + + def update_context_data(self, data, changes): + print(data, changes) + + def get_context_data(self): + return {} def on_pyblish_instance_toggled(instance, old_value, new_value): @@ -254,3 +282,43 @@ def parse_container(tool): return container +def list_instances(creator_id=None): + """Return created instances in current workfile which will be published. + + Returns: + (list) of dictionaries matching instances format + """ + + comp = get_current_comp() + tools = comp.GetToolList(False, "Loader").values() + + instance_signature = { + "id": "pyblish.avalon.instance", + "identifier": creator_id + } + instances = [] + for tool in tools: + + data = tool.GetData('openpype') + if not isinstance(data, dict): + return + + if creator_id and data.get("identifier") != creator_id: + continue + + if data.get("id") != instance_signature["id"]: + continue + + instances.append(tool) + + return instances + + +def remove_instance(instance): + """Remove instance from current workfile. + + Args: + instance (dict): instance representation from subsetmanager model + """ + # Assume instance is a Fusion tool directly + instance.Delete() diff --git a/openpype/hosts/fusion/api/workio.py b/openpype/hosts/fusion/api/workio.py deleted file mode 100644 index 939b2ff4be..0000000000 --- a/openpype/hosts/fusion/api/workio.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Host API required Work Files tool""" -import sys -import os - -from .lib import get_current_comp - - -def file_extensions(): - return [".comp"] - - -def has_unsaved_changes(): - comp = get_current_comp() - return comp.GetAttrs()["COMPB_Modified"] - - -def save_file(filepath): - comp = get_current_comp() - comp.Save(filepath) - - -def open_file(filepath): - # Hack to get fusion, see - # openpype.hosts.fusion.api.pipeline.get_current_comp() - fusion = getattr(sys.modules["__main__"], "fusion", None) - - return fusion.LoadComp(filepath) - - -def current_file(): - comp = get_current_comp() - current_filepath = comp.GetAttrs()["COMPS_FileName"] - if not current_filepath: - return None - - return current_filepath - - -def work_root(session): - work_dir = session["AVALON_WORKDIR"] - scene_dir = session.get("AVALON_SCENEDIR") - if scene_dir: - return os.path.join(work_dir, scene_dir) - else: - return work_dir diff --git a/openpype/hosts/fusion/deploy/MenuScripts/openpype_menu.py b/openpype/hosts/fusion/deploy/MenuScripts/openpype_menu.py index 2918c552c8..685e58d58f 100644 --- a/openpype/hosts/fusion/deploy/MenuScripts/openpype_menu.py +++ b/openpype/hosts/fusion/deploy/MenuScripts/openpype_menu.py @@ -13,11 +13,11 @@ def main(env): # However the contents of that folder can conflict with Qt library dlls # so we make sure to move out of it to avoid DLL Load Failed errors. os.chdir("..") - from openpype.hosts.fusion import api + from openpype.hosts.fusion.api import FusionHost from openpype.hosts.fusion.api import menu # activate resolve from pype - install_host(api) + install_host(FusionHost()) log = Logger.get_logger(__name__) log.info(f"Registered host: {registered_host()}") diff --git a/openpype/hosts/fusion/plugins/create/create_exr_saver.py b/openpype/hosts/fusion/plugins/create/create_exr_saver.py index 6d93fe710a..74cd1cbea5 100644 --- a/openpype/hosts/fusion/plugins/create/create_exr_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_exr_saver.py @@ -1,24 +1,29 @@ import os -from openpype.pipeline import ( - LegacyCreator, - legacy_io -) from openpype.hosts.fusion.api import ( get_current_comp, - comp_lock_and_undo_chunk + comp_lock_and_undo_chunk, + remove_instance, + list_instances +) + +from openpype.pipeline import ( + legacy_io, + Creator, + CreatedInstance ) -class CreateOpenEXRSaver(LegacyCreator): - +class CreateOpenEXRSaver(Creator): + identifier = "io.openpype.creators.fusion.saver" name = "openexrDefault" label = "Create OpenEXR Saver" - hosts = ["fusion"] family = "render" - defaults = ["Main"] + default_variants = ["Main"] - def process(self): + selected_nodes = [] + + def create(self, subset_name, instance_data, pre_create_data): file_format = "OpenEXRFormat" @@ -26,13 +31,13 @@ class CreateOpenEXRSaver(LegacyCreator): workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) - filename = "{}..exr".format(self.name) + filename = "{}..exr".format(subset_name) filepath = os.path.join(workdir, "render", filename) with comp_lock_and_undo_chunk(comp): args = (-32768, -32768) # Magical position numbers saver = comp.AddTool("Saver", *args) - saver.SetAttrs({"TOOLS_Name": self.name}) + saver.SetAttrs({"TOOLS_Name": subset_name}) # Setting input attributes is different from basic attributes # Not confused with "MainInputAttributes" which @@ -47,3 +52,18 @@ class CreateOpenEXRSaver(LegacyCreator): # Set file format attributes saver[file_format]["Depth"] = 1 # int8 | int16 | float32 | other saver[file_format]["SaveAlpha"] = 0 + + def collect_instances(self): + for instance in list_instances(creator_id=self.identifier): + created_instance = CreatedInstance.from_existing(instance, self) + self._add_instance_to_context(created_instance) + + def update_instances(self, update_list): + print(update_list) + + def remove_instances(self, instances): + for instance in instances: + remove_instance(instance) + + def get_pre_create_attr_defs(self): + return [] \ No newline at end of file From 13cb1f4bc0756eae2cf1cf7d07726e137a03a37f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 23:37:32 +0200 Subject: [PATCH 02/30] Ensure newline --- openpype/hosts/fusion/plugins/create/create_exr_saver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_exr_saver.py b/openpype/hosts/fusion/plugins/create/create_exr_saver.py index 74cd1cbea5..4809832cd0 100644 --- a/openpype/hosts/fusion/plugins/create/create_exr_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_exr_saver.py @@ -66,4 +66,4 @@ class CreateOpenEXRSaver(Creator): remove_instance(instance) def get_pre_create_attr_defs(self): - return [] \ No newline at end of file + return [] From 218c836026fcb99dd157fa56e6dfabc8bf3a8271 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 09:12:41 +0200 Subject: [PATCH 03/30] Reorder logic - makes more sense to check id first --- openpype/hosts/fusion/api/pipeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index b73759fee0..f5900a2dde 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -303,10 +303,10 @@ def list_instances(creator_id=None): if not isinstance(data, dict): return - if creator_id and data.get("identifier") != creator_id: + if data.get("id") != instance_signature["id"]: continue - if data.get("id") != instance_signature["id"]: + if creator_id and data.get("identifier") != creator_id: continue instances.append(tool) From 1cf86a68f7b1961118f127cadc068e6b37e62d77 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 14:19:56 +0200 Subject: [PATCH 04/30] Remove creator in menu in favor of new publisher --- openpype/hosts/fusion/api/menu.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 4e415cafba..ec8747298c 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -52,7 +52,6 @@ class OpenPypeMenu(QtWidgets.QWidget): asset_label.setAlignment(QtCore.Qt.AlignHCenter) workfiles_btn = QtWidgets.QPushButton("Workfiles...", self) - create_btn = QtWidgets.QPushButton("Create...", self) publish_btn = QtWidgets.QPushButton("Publish...", self) load_btn = QtWidgets.QPushButton("Load...", self) manager_btn = QtWidgets.QPushButton("Manage...", self) @@ -75,7 +74,6 @@ class OpenPypeMenu(QtWidgets.QWidget): layout.addSpacing(20) - layout.addWidget(create_btn) layout.addWidget(load_btn) layout.addWidget(publish_btn) layout.addWidget(manager_btn) @@ -100,7 +98,6 @@ class OpenPypeMenu(QtWidgets.QWidget): self.asset_label = asset_label workfiles_btn.clicked.connect(self.on_workfile_clicked) - create_btn.clicked.connect(self.on_create_clicked) publish_btn.clicked.connect(self.on_publish_clicked) load_btn.clicked.connect(self.on_load_clicked) manager_btn.clicked.connect(self.on_manager_clicked) @@ -140,9 +137,6 @@ class OpenPypeMenu(QtWidgets.QWidget): def on_workfile_clicked(self): host_tools.show_workfiles() - def on_create_clicked(self): - host_tools.show_creator() - def on_publish_clicked(self): host_tools.show_publisher() From ae5c565ab61609a3affcecc7b398ee9d0a6a874f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 14:20:50 +0200 Subject: [PATCH 05/30] Fix logic - add comments that these will remain unused however --- openpype/hosts/fusion/api/pipeline.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index f5900a2dde..1587381b1a 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -282,6 +282,7 @@ def parse_container(tool): return container +# TODO: Function below is currently unused prototypes def list_instances(creator_id=None): """Return created instances in current workfile which will be published. @@ -290,7 +291,7 @@ def list_instances(creator_id=None): """ comp = get_current_comp() - tools = comp.GetToolList(False, "Loader").values() + tools = comp.GetToolList(False).values() instance_signature = { "id": "pyblish.avalon.instance", @@ -301,7 +302,7 @@ def list_instances(creator_id=None): data = tool.GetData('openpype') if not isinstance(data, dict): - return + continue if data.get("id") != instance_signature["id"]: continue @@ -314,6 +315,7 @@ def list_instances(creator_id=None): return instances +# TODO: Function below is currently unused prototypes def remove_instance(instance): """Remove instance from current workfile. @@ -321,4 +323,4 @@ def remove_instance(instance): instance (dict): instance representation from subsetmanager model """ # Assume instance is a Fusion tool directly - instance.Delete() + instance["tool"].Delete() From d62e1eef82bbb84902a96d1d48fa26538f9a2f81 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 14:21:28 +0200 Subject: [PATCH 06/30] Continue refactor to new publisher --- .../fusion/plugins/create/create_exr_saver.py | 74 ++++++++++++++++++- .../plugins/publish/collect_instances.py | 48 ++++-------- 2 files changed, 86 insertions(+), 36 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_exr_saver.py b/openpype/hosts/fusion/plugins/create/create_exr_saver.py index 4809832cd0..c2adb48bac 100644 --- a/openpype/hosts/fusion/plugins/create/create_exr_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_exr_saver.py @@ -1,5 +1,7 @@ import os +import qtawesome + from openpype.hosts.fusion.api import ( get_current_comp, comp_lock_and_undo_chunk, @@ -19,7 +21,9 @@ class CreateOpenEXRSaver(Creator): name = "openexrDefault" label = "Create OpenEXR Saver" family = "render" - default_variants = ["Main"] + default_variants = ["Main"] + + description = "Fusion Saver to generate EXR image sequence" selected_nodes = [] @@ -27,6 +31,10 @@ class CreateOpenEXRSaver(Creator): file_format = "OpenEXRFormat" + print(subset_name) + print(instance_data) + print(pre_create_data) + comp = get_current_comp() workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) @@ -53,17 +61,79 @@ class CreateOpenEXRSaver(Creator): saver[file_format]["Depth"] = 1 # int8 | int16 | float32 | other saver[file_format]["SaveAlpha"] = 0 + # Save all data in a "openpype.{key}" = value data + for key, value in instance_data.items(): + saver.SetData("openpype.{}".format(key), value) + def collect_instances(self): - for instance in list_instances(creator_id=self.identifier): + + comp = get_current_comp() + tools = comp.GetToolList(False, "Saver").values() + + # Allow regular non-managed savers to also be picked up + project = legacy_io.Session["AVALON_PROJECT"] + asset = legacy_io.Session["AVALON_ASSET"] + task = legacy_io.Session["AVALON_TASK"] + + for tool in tools: + + path = tool["Clip"][comp.TIME_UNDEFINED] + fname = os.path.basename(path) + fname, _ext = os.path.splitext(fname) + subset = fname.rstrip(".") + + attrs = tool.GetAttrs() + passthrough = attrs["TOOLB_PassThrough"] + variant = subset[len("render"):] + + # TODO: this should not be done this way - this should actually + # get the data as stored on the tool explicitly (however) + # that would disallow any 'regular saver' to be collected + # unless the instance data is stored on it to begin with + instance = { + # Required data + "project": project, + "asset": asset, + "subset": subset, + "task": task, + "variant": variant, + "active": not passthrough, + "family": self.family, + + # Fusion data + "tool_name": tool.Name + } + + # Use the explicit data on the saver (if any) + data = tool.GetData("openpype") + if data: + instance.update(data) + + # Add instance created_instance = CreatedInstance.from_existing(instance, self) + + # TODO: move this to lifetime data or alike + # (Doing this before CreatedInstance.from_existing wouldn't + # work because `tool` isn't JSON serializable) + created_instance["tool"] = tool + self._add_instance_to_context(created_instance) + def get_icon(self): + return qtawesome.icon("fa.eye", color="white") + def update_instances(self, update_list): + # TODO: Not sure what to do here? print(update_list) def remove_instances(self, instances): for instance in instances: + + # Remove the tool from the scene remove_instance(instance) + # Remove the collected CreatedInstance to remove from UI directly + self._remove_instance_from_context(instance) + def get_pre_create_attr_defs(self): return [] diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index fe60b83827..e42e7b5f70 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -30,7 +30,7 @@ class CollectInstances(pyblish.api.ContextPlugin): """ order = pyblish.api.CollectorOrder - label = "Collect Instances" + label = "Collect Instances Data" hosts = ["fusion"] def process(self, context): @@ -39,67 +39,47 @@ class CollectInstances(pyblish.api.ContextPlugin): from openpype.hosts.fusion.api.lib import get_frame_path comp = context.data["currentComp"] - - # Get all savers in the comp - tools = comp.GetToolList(False).values() - savers = [tool for tool in tools if tool.ID == "Saver"] - start, end, global_start, global_end = get_comp_render_range(comp) context.data["frameStart"] = int(start) context.data["frameEnd"] = int(end) context.data["frameStartHandle"] = int(global_start) context.data["frameEndHandle"] = int(global_end) - for tool in savers: + # Comp tools by name + tools = {tool.Name: tool for tool in comp.GetToolList(False).values()} + + for instance in context: + + tool_name = instance.data["tool_name"] + tool = tools[tool_name] + path = tool["Clip"][comp.TIME_UNDEFINED] - - tool_attrs = tool.GetAttrs() - active = not tool_attrs["TOOLB_PassThrough"] - - if not path: - self.log.warning("Skipping saver because it " - "has no path set: {}".format(tool.Name)) - continue - filename = os.path.basename(path) head, padding, tail = get_frame_path(filename) ext = os.path.splitext(path)[1] assert tail == ext, ("Tail does not match %s" % ext) - subset = head.rstrip("_. ") # subset is head of the filename # Include start and end render frame in label + subset = instance.data["subset"] label = "{subset} ({start}-{end})".format(subset=subset, start=int(start), end=int(end)) - - instance = context.create_instance(subset) instance.data.update({ - "asset": os.environ["AVALON_ASSET"], # todo: not a constant - "subset": subset, "path": path, "outputDir": os.path.dirname(path), - "ext": ext, # todo: should be redundant + "ext": ext, # todo: should be redundant? "label": label, + # todo: Allow custom frame range per instance "frameStart": context.data["frameStart"], "frameEnd": context.data["frameEnd"], "frameStartHandle": context.data["frameStartHandle"], "frameEndHandle": context.data["frameStartHandle"], "fps": context.data["fps"], "families": ["render", "review"], - "family": "render", - "active": active, - "publish": active # backwards compatibility + "family": "render" }) + # Add tool itself as member instance.append(tool) self.log.info("Found: \"%s\" " % path) - - # Sort/grouped by family (preserving local index) - context[:] = sorted(context, key=self.sort_by_family) - - return context - - def sort_by_family(self, instance): - """Sort by family""" - return instance.data.get("families", instance.data.get("family")) From 33cf1c3089cfa91601e9a10afc8df926ef5db95a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 19:57:45 +0200 Subject: [PATCH 07/30] Cleanup and fixes --- .../fusion/plugins/create/create_exr_saver.py | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_exr_saver.py b/openpype/hosts/fusion/plugins/create/create_exr_saver.py index c2adb48bac..e0366c6532 100644 --- a/openpype/hosts/fusion/plugins/create/create_exr_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_exr_saver.py @@ -4,9 +4,7 @@ import qtawesome from openpype.hosts.fusion.api import ( get_current_comp, - comp_lock_and_undo_chunk, - remove_instance, - list_instances + comp_lock_and_undo_chunk ) from openpype.pipeline import ( @@ -16,25 +14,20 @@ from openpype.pipeline import ( ) -class CreateOpenEXRSaver(Creator): +class CreateSaver(Creator): identifier = "io.openpype.creators.fusion.saver" - name = "openexrDefault" - label = "Create OpenEXR Saver" + name = "saver" + label = "Create Saver" family = "render" default_variants = ["Main"] - description = "Fusion Saver to generate EXR image sequence" - - selected_nodes = [] + description = "Fusion Saver to generate image sequence" def create(self, subset_name, instance_data, pre_create_data): + # TODO: Add pre_create attributes to choose file format? file_format = "OpenEXRFormat" - print(subset_name) - print(instance_data) - print(pre_create_data) - comp = get_current_comp() workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) @@ -61,9 +54,7 @@ class CreateOpenEXRSaver(Creator): saver[file_format]["Depth"] = 1 # int8 | int16 | float32 | other saver[file_format]["SaveAlpha"] = 0 - # Save all data in a "openpype.{key}" = value data - for key, value in instance_data.items(): - saver.SetData("openpype.{}".format(key), value) + self._imprint(saver, instance_data) def collect_instances(self): @@ -112,28 +103,42 @@ class CreateOpenEXRSaver(Creator): # Add instance created_instance = CreatedInstance.from_existing(instance, self) - # TODO: move this to lifetime data or alike - # (Doing this before CreatedInstance.from_existing wouldn't - # work because `tool` isn't JSON serializable) - created_instance["tool"] = tool - self._add_instance_to_context(created_instance) def get_icon(self): return qtawesome.icon("fa.eye", color="white") def update_instances(self, update_list): - # TODO: Not sure what to do here? - print(update_list) + for update in update_list: + instance = update.instance + changes = update.changes + tool = self._get_instance_tool(instance) + self._imprint(tool, changes) def remove_instances(self, instances): for instance in instances: - # Remove the tool from the scene - remove_instance(instance) + tool = self._get_instance_tool(instance) + if tool: + tool.Delete() # Remove the collected CreatedInstance to remove from UI directly self._remove_instance_from_context(instance) - def get_pre_create_attr_defs(self): - return [] + def _imprint(self, tool, data): + + # Save all data in a "openpype.{key}" = value data + for key, value in data.items(): + tool.SetData("openpype.{}".format(key), value) + + def _get_instance_tool(self, instance): + # finds tool name of instance in currently active comp + # TODO: assign `tool` as some sort of lifetime data or alike so that + # the actual tool can be retrieved in current session. We can't store + # it in the instance itself since instance needs to be serializable + comp = get_current_comp() + tool_name = instance["tool_name"] + print(tool_name) + return { + tool.Name: tool for tool in comp.GetToolList(False).values() + }.get(tool_name) From 450a471c42a13d670eaf96a4323c92629c0b20a5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 19:58:42 +0200 Subject: [PATCH 08/30] Rename `create_exr_saver.py` -> `create_saver.py` --- .../plugins/create/{create_exr_saver.py => create_saver.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/hosts/fusion/plugins/create/{create_exr_saver.py => create_saver.py} (100%) diff --git a/openpype/hosts/fusion/plugins/create/create_exr_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py similarity index 100% rename from openpype/hosts/fusion/plugins/create/create_exr_saver.py rename to openpype/hosts/fusion/plugins/create/create_saver.py From 7a046dd446bef43606317df47c487b7809701a81 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 20:02:58 +0200 Subject: [PATCH 09/30] Register the saver directly on create --- openpype/hosts/fusion/plugins/create/create_saver.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index e0366c6532..a0ab1c1fcf 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -56,6 +56,16 @@ class CreateSaver(Creator): self._imprint(saver, instance_data) + # Register the CreatedInstance + instance = CreatedInstance( + family=self.family, + subset_name=subset_name, + instance_data=instance_data, + creator=self) + self._add_instance_to_context(instance) + + return instance + def collect_instances(self): comp = get_current_comp() From cd0825756e753de983c651413affdcaad110120a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 20:04:17 +0200 Subject: [PATCH 10/30] Fix keyword --- openpype/hosts/fusion/plugins/create/create_saver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index a0ab1c1fcf..c2c9ad1cb7 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -60,7 +60,7 @@ class CreateSaver(Creator): instance = CreatedInstance( family=self.family, subset_name=subset_name, - instance_data=instance_data, + data=instance_data, creator=self) self._add_instance_to_context(instance) From 01167e84caf8e1e047ecffc93d7350b34d8ada46 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 23:11:05 +0200 Subject: [PATCH 11/30] Barebones refactor of validators to raise PublishValidationError --- .../publish/validate_background_depth.py | 7 +++- .../plugins/publish/validate_comp_saved.py | 7 +++- .../publish/validate_create_folder_checked.py | 6 ++- .../validate_filename_has_extension.py | 4 +- .../publish/validate_saver_has_input.py | 8 +++- .../publish/validate_saver_passthrough.py | 7 ++-- .../publish/validate_unique_subsets.py | 41 ++++++++++++++----- 7 files changed, 57 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py index 4268fab528..f057989535 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py +++ b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py @@ -1,6 +1,7 @@ import pyblish.api from openpype.pipeline.publish import RepairAction +from openpype.pipeline import PublishValidationError class ValidateBackgroundDepth(pyblish.api.InstancePlugin): @@ -29,8 +30,10 @@ class ValidateBackgroundDepth(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Found %i nodes which are not set to float32" - % len(invalid)) + raise PublishValidationError( + "Found {} Backgrounds tools which" + " are not set to float32".format(len(invalid)), + title=self.label) @classmethod def repair(cls, instance): diff --git a/openpype/hosts/fusion/plugins/publish/validate_comp_saved.py b/openpype/hosts/fusion/plugins/publish/validate_comp_saved.py index cabe65af6e..748047e8cf 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_comp_saved.py +++ b/openpype/hosts/fusion/plugins/publish/validate_comp_saved.py @@ -1,6 +1,7 @@ import os import pyblish.api +from openpype.pipeline import PublishValidationError class ValidateFusionCompSaved(pyblish.api.ContextPlugin): @@ -19,10 +20,12 @@ class ValidateFusionCompSaved(pyblish.api.ContextPlugin): filename = attrs["COMPS_FileName"] if not filename: - raise RuntimeError("Comp is not saved.") + raise PublishValidationError("Comp is not saved.", + title=self.label) if not os.path.exists(filename): - raise RuntimeError("Comp file does not exist: %s" % filename) + raise PublishValidationError( + "Comp file does not exist: %s" % filename, title=self.label) if attrs["COMPB_Modified"]: self.log.warning("Comp is modified. Save your comp to ensure your " diff --git a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py index f6beefefc1..3674b33644 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py +++ b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py @@ -1,6 +1,7 @@ import pyblish.api from openpype.pipeline.publish import RepairAction +from openpype.pipeline import PublishValidationError class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): @@ -31,8 +32,9 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Found Saver with Create Folder During " - "Render checked off") + raise PublishValidationError( + "Found Saver with Create Folder During Render checked off", + title=self.label) @classmethod def repair(cls, instance): diff --git a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py index 4795a2aa05..22f1db809c 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py +++ b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py @@ -1,6 +1,7 @@ import os import pyblish.api +from openpype.pipeline import PublishValidationError class ValidateFilenameHasExtension(pyblish.api.InstancePlugin): @@ -20,7 +21,8 @@ class ValidateFilenameHasExtension(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Found Saver without an extension") + raise PublishValidationError("Found Saver without an extension", + title=self.label) @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py index 7243b44a3e..8d961525f0 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py @@ -1,4 +1,5 @@ import pyblish.api +from openpype.pipeline import PublishValidationError class ValidateSaverHasInput(pyblish.api.InstancePlugin): @@ -25,5 +26,8 @@ class ValidateSaverHasInput(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Saver has no incoming connection: " - "{} ({})".format(instance, invalid[0].Name)) + saver_name = invalid[0].Name + raise PublishValidationError( + "Saver has no incoming connection: {} ({})".format(instance, + saver_name), + title=self.label) diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py index aed3835de3..c191d6669c 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py @@ -1,4 +1,5 @@ import pyblish.api +from openpype.pipeline import PublishValidationError class ValidateSaverPassthrough(pyblish.api.ContextPlugin): @@ -27,8 +28,8 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin): if invalid_instances: self.log.info("Reset pyblish to collect your current scene state, " "that should fix error.") - raise RuntimeError("Invalid instances: " - "{0}".format(invalid_instances)) + raise PublishValidationError( + "Invalid instances: {0}".format(invalid_instances)) def is_invalid(self, instance): @@ -36,7 +37,7 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin): attr = saver.GetAttrs() active = not attr["TOOLB_PassThrough"] - if active != instance.data["publish"]: + if active != instance.data.get("publish", True): self.log.info("Saver has different passthrough state than " "Pyblish: {} ({})".format(instance, saver.Name)) return [saver] diff --git a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py index b218a311ba..b78f185a3a 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py @@ -1,7 +1,10 @@ +from collections import defaultdict + import pyblish.api +from openpype.pipeline import PublishValidationError -class ValidateUniqueSubsets(pyblish.api.InstancePlugin): +class ValidateUniqueSubsets(pyblish.api.ContextPlugin): """Ensure all instances have a unique subset name""" order = pyblish.api.ValidatorOrder @@ -10,20 +13,36 @@ class ValidateUniqueSubsets(pyblish.api.InstancePlugin): hosts = ["fusion"] @classmethod - def get_invalid(cls, instance): + def get_invalid(cls, context): - context = instance.context - subset = instance.data["subset"] - for other_instance in context: - if other_instance == instance: - continue + # Collect instances per subset per asset + instances_per_subset_asset = defaultdict(lambda: defaultdict(list)) + for instance in context: + asset = instance.data.get("asset", context.data.get("asset")) + subset = instance.data.get("subset", context.data.get("subset")) + instances_per_subset_asset[asset][subset].append(instance) - if other_instance.data["subset"] == subset: - return [instance] # current instance is invalid + # Find which asset + subset combination has more than one instance + # Those are considered invalid because they'd integrate to the same + # destination. + invalid = [] + for asset, instances_per_subset in instances_per_subset_asset.items(): + for subset, instances in instances_per_subset.items(): + if len(instances) > 1: + cls.log.warning( + "{asset} > {subset} used by more than " + "one instance: {instances}".format( + asset=asset, + subset=subset, + instances=instances + )) + invalid.extend(instances) - return [] + return invalid def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Animation content is invalid. See log.") + raise PublishValidationError("Multiple instances are set to " + "the same asset > subset.", + title=self.label) From ce954651182b2f4c526f981fcd5c086989f23b36 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 23:17:31 +0200 Subject: [PATCH 12/30] Fix bugs in Creator --- .../fusion/plugins/create/create_saver.py | 154 +++++++++++------- 1 file changed, 99 insertions(+), 55 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index c2c9ad1cb7..347aaaf497 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -29,20 +29,12 @@ class CreateSaver(Creator): file_format = "OpenEXRFormat" comp = get_current_comp() - - workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) - - filename = "{}..exr".format(subset_name) - filepath = os.path.join(workdir, "render", filename) - with comp_lock_and_undo_chunk(comp): args = (-32768, -32768) # Magical position numbers saver = comp.AddTool("Saver", *args) - saver.SetAttrs({"TOOLS_Name": subset_name}) - # Setting input attributes is different from basic attributes - # Not confused with "MainInputAttributes" which - saver["Clip"] = filepath + self._update_tool_with_data(saver, data=instance_data) + saver["OutputFormat"] = file_format # Check file format settings are available @@ -54,6 +46,9 @@ class CreateSaver(Creator): saver[file_format]["Depth"] = 1 # int8 | int16 | float32 | other saver[file_format]["SaveAlpha"] = 0 + # Fusion data for the instance data + instance_data["tool_name"] = saver.Name + self._imprint(saver, instance_data) # Register the CreatedInstance @@ -70,49 +65,17 @@ class CreateSaver(Creator): comp = get_current_comp() tools = comp.GetToolList(False, "Saver").values() - - # Allow regular non-managed savers to also be picked up - project = legacy_io.Session["AVALON_PROJECT"] - asset = legacy_io.Session["AVALON_ASSET"] - task = legacy_io.Session["AVALON_TASK"] - for tool in tools: - path = tool["Clip"][comp.TIME_UNDEFINED] - fname = os.path.basename(path) - fname, _ext = os.path.splitext(fname) - subset = fname.rstrip(".") + data = self.get_managed_tool_data(tool) + if not data: + data = self._collect_unmanaged_saver(tool) - attrs = tool.GetAttrs() - passthrough = attrs["TOOLB_PassThrough"] - variant = subset[len("render"):] - - # TODO: this should not be done this way - this should actually - # get the data as stored on the tool explicitly (however) - # that would disallow any 'regular saver' to be collected - # unless the instance data is stored on it to begin with - instance = { - # Required data - "project": project, - "asset": asset, - "subset": subset, - "task": task, - "variant": variant, - "active": not passthrough, - "family": self.family, - - # Fusion data - "tool_name": tool.Name - } - - # Use the explicit data on the saver (if any) - data = tool.GetData("openpype") - if data: - instance.update(data) + # Collect non-stored data + data["tool_name"] = tool.Name # Add instance - created_instance = CreatedInstance.from_existing(instance, self) - + created_instance = CreatedInstance.from_existing(data, self) self._add_instance_to_context(created_instance) def get_icon(self): @@ -121,9 +84,18 @@ class CreateSaver(Creator): def update_instances(self, update_list): for update in update_list: instance = update.instance - changes = update.changes + + # Get the new values after the changes by key, ignore old value + new_data = { + key: new for key, (_old, new) in update.changes.items() + } + tool = self._get_instance_tool(instance) - self._imprint(tool, changes) + self._update_tool_with_data(tool, new_data) + self._imprint(tool, new_data) + + # Ensure tool name is up-to-date + instance["tool_name"] = tool.Name def remove_instances(self, instances): for instance in instances: @@ -136,19 +108,91 @@ class CreateSaver(Creator): self._remove_instance_from_context(instance) def _imprint(self, tool, data): - # Save all data in a "openpype.{key}" = value data for key, value in data.items(): tool.SetData("openpype.{}".format(key), value) def _get_instance_tool(self, instance): # finds tool name of instance in currently active comp - # TODO: assign `tool` as some sort of lifetime data or alike so that - # the actual tool can be retrieved in current session. We can't store - # it in the instance itself since instance needs to be serializable + # TODO: assign `tool` as 'lifetime' data instead of name so the + # tool can be retrieved in current session. We can't store currently + # in the CreatedInstance data because it needs to be serializable comp = get_current_comp() tool_name = instance["tool_name"] - print(tool_name) return { tool.Name: tool for tool in comp.GetToolList(False).values() }.get(tool_name) + + def _update_tool_with_data(self, tool, data): + """Update tool node name and output path based on subset data""" + if "subset" not in data: + return + + original_subset = tool.GetData("openpype.subset") + subset = data["subset"] + if original_subset != subset: + # Subset change detected + # Update output filepath + workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) + filename = "{}..exr".format(subset) + filepath = os.path.join(workdir, "render", subset, filename) + tool["Clip"] = filepath + + # Rename tool + if tool.Name != subset: + print(f"Renaming {tool.Name} -> {subset}") + tool.SetAttrs({"TOOLS_Name": subset}) + + def _collect_unmanaged_saver(self, tool): + + # TODO: this should not be done this way - this should actually + # get the data as stored on the tool explicitly (however) + # that would disallow any 'regular saver' to be collected + # unless the instance data is stored on it to begin with + + print("Collecting unmanaged saver..") + comp = tool.Comp() + + # Allow regular non-managed savers to also be picked up + project = legacy_io.Session["AVALON_PROJECT"] + asset = legacy_io.Session["AVALON_ASSET"] + task = legacy_io.Session["AVALON_TASK"] + + path = tool["Clip"][comp.TIME_UNDEFINED] + fname = os.path.basename(path) + fname, _ext = os.path.splitext(fname) + subset = fname.rstrip(".") + + attrs = tool.GetAttrs() + passthrough = attrs["TOOLB_PassThrough"] + variant = subset[len("render"):] + return { + # Required data + "project": project, + "asset": asset, + "subset": subset, + "task": task, + "variant": variant, + "active": not passthrough, + "family": self.family, + + # Unique identifier for instance and this creator + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier + } + + def get_managed_tool_data(self, tool): + """Return data of the tool if it matches creator identifier""" + data = tool.GetData('openpype') + if not isinstance(data, dict): + return + + required = { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier + } + for key, value in required.items(): + if key not in data or data[key] != value: + return + + return data From af8662c87525012a8c7fd9915dcc1f2ce725c967 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 23:24:28 +0200 Subject: [PATCH 13/30] Fix refactor to ContextPlugin --- .../hosts/fusion/plugins/publish/validate_unique_subsets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py index b78f185a3a..5f0f93f764 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py @@ -40,8 +40,8 @@ class ValidateUniqueSubsets(pyblish.api.ContextPlugin): return invalid - def process(self, instance): - invalid = self.get_invalid(instance) + def process(self, context): + invalid = self.get_invalid(context) if invalid: raise PublishValidationError("Multiple instances are set to " "the same asset > subset.", From fe857b84429cda78235084d16c32ba3e58701aba Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 23:26:03 +0200 Subject: [PATCH 14/30] Add title to error --- .../hosts/fusion/plugins/publish/validate_saver_passthrough.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py index c191d6669c..bbafd8949e 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py @@ -29,7 +29,8 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin): self.log.info("Reset pyblish to collect your current scene state, " "that should fix error.") raise PublishValidationError( - "Invalid instances: {0}".format(invalid_instances)) + "Invalid instances: {0}".format(invalid_instances), + title=self.label) def is_invalid(self, instance): From 6abfabed4057292419059fa193bf5ffcbc8b9d21 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 23:33:13 +0200 Subject: [PATCH 15/30] Fix missing import --- openpype/hosts/fusion/api/pipeline.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 1587381b1a..6fc3902949 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -2,6 +2,7 @@ Basic avalon integration """ import os +import sys import logging import contextlib From bdfe2414583480c06f0ee35113b0deea3fced1c6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 23 Sep 2022 17:31:35 +0200 Subject: [PATCH 16/30] Refactor new publish logic to make use of "transientData" on the Creator --- .../fusion/plugins/create/create_saver.py | 33 +++++++------------ .../plugins/publish/collect_instances.py | 11 +++---- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 347aaaf497..b3a912c56a 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -46,9 +46,6 @@ class CreateSaver(Creator): saver[file_format]["Depth"] = 1 # int8 | int16 | float32 | other saver[file_format]["SaveAlpha"] = 0 - # Fusion data for the instance data - instance_data["tool_name"] = saver.Name - self._imprint(saver, instance_data) # Register the CreatedInstance @@ -57,6 +54,10 @@ class CreateSaver(Creator): subset_name=subset_name, data=instance_data, creator=self) + + # Insert the transient data + instance.transient_data["tool"] = saver + self._add_instance_to_context(instance) return instance @@ -71,11 +72,12 @@ class CreateSaver(Creator): if not data: data = self._collect_unmanaged_saver(tool) - # Collect non-stored data - data["tool_name"] = tool.Name - # Add instance created_instance = CreatedInstance.from_existing(data, self) + + # Collect transient data + created_instance.transient_data["tool"] = tool + self._add_instance_to_context(created_instance) def get_icon(self): @@ -90,17 +92,15 @@ class CreateSaver(Creator): key: new for key, (_old, new) in update.changes.items() } - tool = self._get_instance_tool(instance) + tool = instance.transient_data["tool"] self._update_tool_with_data(tool, new_data) self._imprint(tool, new_data) - # Ensure tool name is up-to-date - instance["tool_name"] = tool.Name - def remove_instances(self, instances): for instance in instances: # Remove the tool from the scene - tool = self._get_instance_tool(instance) + + tool = instance.transient_data["tool"] if tool: tool.Delete() @@ -112,17 +112,6 @@ class CreateSaver(Creator): for key, value in data.items(): tool.SetData("openpype.{}".format(key), value) - def _get_instance_tool(self, instance): - # finds tool name of instance in currently active comp - # TODO: assign `tool` as 'lifetime' data instead of name so the - # tool can be retrieved in current session. We can't store currently - # in the CreatedInstance data because it needs to be serializable - comp = get_current_comp() - tool_name = instance["tool_name"] - return { - tool.Name: tool for tool in comp.GetToolList(False).values() - }.get(tool_name) - def _update_tool_with_data(self, tool, data): """Update tool node name and output path based on subset data""" if "subset" not in data: diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index e42e7b5f70..2f3e82fded 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -45,13 +45,9 @@ class CollectInstances(pyblish.api.ContextPlugin): context.data["frameStartHandle"] = int(global_start) context.data["frameEndHandle"] = int(global_end) - # Comp tools by name - tools = {tool.Name: tool for tool in comp.GetToolList(False).values()} - for instance in context: - tool_name = instance.data["tool_name"] - tool = tools[tool_name] + tool = instance.data["transientData"]["tool"] path = tool["Clip"][comp.TIME_UNDEFINED] filename = os.path.basename(path) @@ -76,7 +72,10 @@ class CollectInstances(pyblish.api.ContextPlugin): "frameEndHandle": context.data["frameStartHandle"], "fps": context.data["fps"], "families": ["render", "review"], - "family": "render" + "family": "render", + + # Backwards compatibility: embed tool in instance.data + "tool": tool }) # Add tool itself as member From 87621c14f5d47de295ae476879c00fbbe7efcf14 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 23 Sep 2022 17:34:47 +0200 Subject: [PATCH 17/30] Refactor `INewPublisher` to `IPublishHost` --- openpype/hosts/fusion/api/pipeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 6fc3902949..3e30c5eaf5 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -20,7 +20,7 @@ from openpype.pipeline import ( ) from openpype.pipeline.load import any_outdated_containers from openpype.hosts.fusion import FUSION_HOST_DIR -from openpype.host import HostBase, IWorkfileHost, ILoadHost, INewPublisher +from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost from openpype.tools.utils import host_tools @@ -48,7 +48,7 @@ class CompLogHandler(logging.Handler): comp.Print(entry) -class FusionHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): +class FusionHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): name = "fusion" def install(self): From 11bd9e8bb1f5a62c501f4fe4388c79fe82d8bc8a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 23 Sep 2022 17:44:05 +0200 Subject: [PATCH 18/30] Add Select Invalid Action --- openpype/hosts/fusion/api/action.py | 54 +++++++++++++++++++ .../publish/validate_saver_has_input.py | 3 ++ 2 files changed, 57 insertions(+) create mode 100644 openpype/hosts/fusion/api/action.py diff --git a/openpype/hosts/fusion/api/action.py b/openpype/hosts/fusion/api/action.py new file mode 100644 index 0000000000..1750920950 --- /dev/null +++ b/openpype/hosts/fusion/api/action.py @@ -0,0 +1,54 @@ +import pyblish.api + + +from openpype.hosts.fusion.api.lib import get_current_comp +from openpype.pipeline.publish import get_errored_instances_from_context + + +class SelectInvalidAction(pyblish.api.Action): + """Select invalid nodes in Maya when plug-in failed. + + To retrieve the invalid nodes this assumes a static `get_invalid()` + method is available on the plugin. + + """ + label = "Select invalid" + 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) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + + # Get the invalid nodes for the plug-ins + self.log.info("Finding invalid nodes..") + invalid = list() + for instance in instances: + invalid_nodes = plugin.get_invalid(instance) + if invalid_nodes: + if isinstance(invalid_nodes, (list, tuple)): + invalid.extend(invalid_nodes) + else: + self.log.warning("Plug-in returned to be invalid, " + "but has no selectable nodes.") + + if not invalid: + # 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 = invalid[0] + comp = first_tool.Comp() + flow = comp.CurrentFrame.FlowView + flow.Select() # No args equals clearing selection + names = set() + for tool in invalid: + flow.Select(tool, True) + names.add(tool.Name) + self.log.info("Selecting invalid tools: %s" % ", ".join(sorted(names))) diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py index 8d961525f0..e02125f531 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py @@ -1,6 +1,8 @@ import pyblish.api from openpype.pipeline import PublishValidationError +from openpype.hosts.fusion.api.action import SelectInvalidAction + class ValidateSaverHasInput(pyblish.api.InstancePlugin): """Validate saver has incoming connection @@ -13,6 +15,7 @@ class ValidateSaverHasInput(pyblish.api.InstancePlugin): label = "Validate Saver Has Input" families = ["render"] hosts = ["fusion"] + actions = [SelectInvalidAction] @classmethod def get_invalid(cls, instance): From 0f1ed036231d91ba6f8105d85b0c6e341e5b6487 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 23 Sep 2022 17:47:19 +0200 Subject: [PATCH 19/30] Add Select Invalid action to more validators --- .../fusion/plugins/publish/validate_background_depth.py | 4 ++++ .../plugins/publish/validate_create_folder_checked.py | 3 +++ .../plugins/publish/validate_filename_has_extension.py | 3 +++ .../fusion/plugins/publish/validate_saver_passthrough.py | 3 +++ .../hosts/fusion/plugins/publish/validate_unique_subsets.py | 6 ++++++ 5 files changed, 19 insertions(+) diff --git a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py index f057989535..261533de01 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py +++ b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py @@ -3,6 +3,8 @@ import pyblish.api from openpype.pipeline.publish import RepairAction from openpype.pipeline import PublishValidationError +from openpype.hosts.fusion.api.action import SelectInvalidAction + class ValidateBackgroundDepth(pyblish.api.InstancePlugin): """Validate if all Background tool are set to float32 bit""" @@ -14,6 +16,8 @@ class ValidateBackgroundDepth(pyblish.api.InstancePlugin): families = ["render"] optional = True + actions = [SelectInvalidAction] + @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py index 3674b33644..ba943abacb 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py +++ b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py @@ -3,6 +3,8 @@ import pyblish.api from openpype.pipeline.publish import RepairAction from openpype.pipeline import PublishValidationError +from openpype.hosts.fusion.api.action import SelectInvalidAction + class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): """Valid if all savers have the input attribute CreateDir checked on @@ -16,6 +18,7 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): label = "Validate Create Folder Checked" families = ["render"] hosts = ["fusion"] + actions = [SelectInvalidAction] @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py index 22f1db809c..bbba2dde6e 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py +++ b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py @@ -3,6 +3,8 @@ import os import pyblish.api from openpype.pipeline import PublishValidationError +from openpype.hosts.fusion.api.action import SelectInvalidAction + class ValidateFilenameHasExtension(pyblish.api.InstancePlugin): """Ensure the Saver has an extension in the filename path @@ -17,6 +19,7 @@ class ValidateFilenameHasExtension(pyblish.api.InstancePlugin): label = "Validate Filename Has Extension" families = ["render"] hosts = ["fusion"] + actions = [SelectInvalidAction] def process(self, instance): invalid = self.get_invalid(instance) diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py index bbafd8949e..56f2e7e6b8 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py @@ -1,6 +1,8 @@ import pyblish.api from openpype.pipeline import PublishValidationError +from openpype.hosts.fusion.api.action import SelectInvalidAction + class ValidateSaverPassthrough(pyblish.api.ContextPlugin): """Validate saver passthrough is similar to Pyblish publish state""" @@ -9,6 +11,7 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin): label = "Validate Saver Passthrough" families = ["render"] hosts = ["fusion"] + actions = [SelectInvalidAction] def process(self, context): diff --git a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py index 5f0f93f764..28bab59949 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py @@ -3,6 +3,8 @@ from collections import defaultdict import pyblish.api from openpype.pipeline import PublishValidationError +from openpype.hosts.fusion.api.action import SelectInvalidAction + class ValidateUniqueSubsets(pyblish.api.ContextPlugin): """Ensure all instances have a unique subset name""" @@ -11,6 +13,7 @@ class ValidateUniqueSubsets(pyblish.api.ContextPlugin): label = "Validate Unique Subsets" families = ["render"] hosts = ["fusion"] + actions = [SelectInvalidAction] @classmethod def get_invalid(cls, context): @@ -38,6 +41,9 @@ class ValidateUniqueSubsets(pyblish.api.ContextPlugin): )) invalid.extend(instances) + # Return tools for the invalid instances so they can be selected + invalid = [instance.data["tool"] for instance in invalid] + return invalid def process(self, context): From a64a551fa19a472c4efe36528051394866f37cf1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 10:56:34 +0200 Subject: [PATCH 20/30] Tweak label --- openpype/hosts/fusion/plugins/create/create_saver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index b3a912c56a..99aa7583f1 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -17,7 +17,7 @@ from openpype.pipeline import ( class CreateSaver(Creator): identifier = "io.openpype.creators.fusion.saver" name = "saver" - label = "Create Saver" + label = "Saver" family = "render" default_variants = ["Main"] From 85cb398b6ee14b66ece629fb446c97ee9fd2347a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 10:59:41 +0200 Subject: [PATCH 21/30] Implement draft for Create Workfile --- .../fusion/plugins/create/create_workfile.py | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 openpype/hosts/fusion/plugins/create/create_workfile.py diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py new file mode 100644 index 0000000000..26a73abb64 --- /dev/null +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -0,0 +1,129 @@ +import collections + +import qtawesome + +import openpype.hosts.fusion.api as api +from openpype.client import get_asset_by_name +from openpype.pipeline import ( + AutoCreator, + CreatedInstance, + legacy_io, +) + + +def flatten_dict(d, parent_key=None, separator="."): + items = [] + for key, v in d.items(): + new_key = parent_key + separator + key if parent_key else key + if isinstance(v, collections.MutableMapping): + items.extend(flatten_dict(v, new_key, separator=separator).items()) + else: + items.append((new_key, v)) + return dict(items) + + +class FusionWorkfileCreator(AutoCreator): + identifier = "workfile" + family = "workfile" + label = "Workfile" + + default_variant = "Main" + + create_allow_context_change = False + + data_key = "openpype.workfile" + + def collect_instances(self): + + comp = api.get_current_comp() + data = comp.GetData(self.data_key) + if not data: + return + + instance = CreatedInstance( + family=self.family, + subset_name=data["subset"], + data=data, + creator=self + ) + instance.transient_data["comp"] = comp + instance.transient_data["tool"] = None + + self._add_instance_to_context(instance) + + def update_instances(self, update_list): + for update in update_list: + instance = update.instance + comp = instance.transient_data["comp"] + if not hasattr(comp, "SetData"): + # Comp is not alive anymore, likely closed by the user + self.log.error("Workfile comp not found for existing instance." + " Comp might have been closed in the meantime.") + continue + + # TODO: It appears sometimes this could be 'nested' + # Get the new values after the changes by key, ignore old value + new_data = { + key: new for key, (_old, new) in update.changes.items() + } + self._imprint(comp, new_data) + + def create(self, options=None): + + comp = api.get_current_comp() + if not comp: + self.log.error("Unable to find current comp") + return + + # TODO: Is this really necessary? + # Force kill any existing "workfile" instances + for instance in self.create_context.instances: + if instance.family == self.family: + self.log.debug(f"Removing instance: {instance}") + self._remove_instance_from_context(instance) + + project_name = legacy_io.Session["AVALON_PROJECT"] + asset_name = legacy_io.Session["AVALON_ASSET"] + task_name = legacy_io.Session["AVALON_TASK"] + host_name = legacy_io.Session["AVALON_APP"] + + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + self.default_variant, task_name, asset_doc, + project_name, host_name + ) + data = { + "asset": asset_name, + "task": task_name, + "variant": self.default_variant + } + data.update(self.get_dynamic_data( + self.default_variant, task_name, asset_doc, + project_name, host_name + )) + + instance = CreatedInstance( + self.family, subset_name, data, self + ) + instance.transient_data["comp"] = comp + instance.transient_data["tool"] = None + self._add_instance_to_context(instance) + + self._imprint(comp, data) + + def get_icon(self): + return qtawesome.icon("fa.file-o", color="white") + + def _imprint(self, comp, data): + + # TODO: Should this keys persist or not? I'd prefer not + # Do not persist the current context for the Workfile + for key in ["variant", "subset", "asset", "task"]: + data.pop(key, None) + + # Flatten any potential nested dicts + data = flatten_dict(data, separator=".") + + # Prefix with data key openpype.workfile + data = {f"{self.data_key}.{key}" for key, value in data.items()} + comp.SetData(data) From f555c1bf9a11200d8a48a9a68bac6f5e4695e347 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 11:00:35 +0200 Subject: [PATCH 22/30] Shush hound --- .../hosts/fusion/plugins/publish/validate_unique_subsets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py index 28bab59949..5b6ceb2fdb 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py @@ -38,7 +38,8 @@ class ValidateUniqueSubsets(pyblish.api.ContextPlugin): asset=asset, subset=subset, instances=instances - )) + ) + ) invalid.extend(instances) # Return tools for the invalid instances so they can be selected From b8f4a0a3969b841f50cb66f15e08717d3d3cd890 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 11:41:55 +0200 Subject: [PATCH 23/30] Specifiy families explicitly (to avoid issues with workfile family not having a `tool`) --- openpype/hosts/fusion/plugins/publish/collect_inputs.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/fusion/plugins/publish/collect_inputs.py b/openpype/hosts/fusion/plugins/publish/collect_inputs.py index 8f9857b02f..e06649da99 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_inputs.py +++ b/openpype/hosts/fusion/plugins/publish/collect_inputs.py @@ -97,10 +97,15 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin): label = "Collect Inputs" order = pyblish.api.CollectorOrder + 0.2 hosts = ["fusion"] + families = ["render"] def process(self, instance): # Get all upstream and include itself + if not any(instance[:]): + self.log.debug("No tool found in instance, skipping..") + return + tool = instance[0] nodes = list(iter_upstream(tool)) nodes.append(tool) From 2afc8ba573ee06ed790146eb113d1a74cbe705b3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 11:42:38 +0200 Subject: [PATCH 24/30] Fix collector with workfile present --- .../plugins/publish/collect_instances.py | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 2f3e82fded..ad264f0478 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -46,24 +46,12 @@ class CollectInstances(pyblish.api.ContextPlugin): context.data["frameEndHandle"] = int(global_end) for instance in context: - - tool = instance.data["transientData"]["tool"] - - path = tool["Clip"][comp.TIME_UNDEFINED] - filename = os.path.basename(path) - head, padding, tail = get_frame_path(filename) - ext = os.path.splitext(path)[1] - assert tail == ext, ("Tail does not match %s" % ext) - # Include start and end render frame in label subset = instance.data["subset"] label = "{subset} ({start}-{end})".format(subset=subset, start=int(start), end=int(end)) instance.data.update({ - "path": path, - "outputDir": os.path.dirname(path), - "ext": ext, # todo: should be redundant? "label": label, # todo: Allow custom frame range per instance "frameStart": context.data["frameStart"], @@ -71,14 +59,32 @@ class CollectInstances(pyblish.api.ContextPlugin): "frameStartHandle": context.data["frameStartHandle"], "frameEndHandle": context.data["frameStartHandle"], "fps": context.data["fps"], - "families": ["render", "review"], - "family": "render", - - # Backwards compatibility: embed tool in instance.data - "tool": tool }) - # Add tool itself as member - instance.append(tool) + if instance.data["family"] == "render": + # TODO: This should probably move into a collector of + # its own for the "render" family + # This is only the case for savers currently but not + # for workfile instances. So we assume saver here. + tool = instance.data["transientData"]["tool"] + path = tool["Clip"][comp.TIME_UNDEFINED] - self.log.info("Found: \"%s\" " % path) + filename = os.path.basename(path) + head, padding, tail = get_frame_path(filename) + ext = os.path.splitext(path)[1] + assert tail == ext, ("Tail does not match %s" % ext) + + instance.data.update({ + "path": path, + "outputDir": os.path.dirname(path), + "ext": ext, # todo: should be redundant? + + "families": ["render", "review"], + "family": "render", + + # Backwards compatibility: embed tool in instance.data + "tool": tool + }) + + # Add tool itself as member + instance.append(tool) From d0ea9171b3cbd2c52ab3e027f496f75e6bb36768 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 11:43:25 +0200 Subject: [PATCH 25/30] Remove storage of empty tool placeholder value --- openpype/hosts/fusion/plugins/create/create_workfile.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index 26a73abb64..19ad04f572 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -47,7 +47,6 @@ class FusionWorkfileCreator(AutoCreator): creator=self ) instance.transient_data["comp"] = comp - instance.transient_data["tool"] = None self._add_instance_to_context(instance) @@ -106,7 +105,6 @@ class FusionWorkfileCreator(AutoCreator): self.family, subset_name, data, self ) instance.transient_data["comp"] = comp - instance.transient_data["tool"] = None self._add_instance_to_context(instance) self._imprint(comp, data) From 3a952d5038b51e7d05e64d70e10054a8c9b5bd4c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 11:43:37 +0200 Subject: [PATCH 26/30] Cosmetics/readability --- openpype/hosts/fusion/plugins/create/create_workfile.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index 19ad04f572..783d3a147a 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -102,7 +102,10 @@ class FusionWorkfileCreator(AutoCreator): )) instance = CreatedInstance( - self.family, subset_name, data, self + family=self.family, + subset_name=subset_name, + data=data, + creator=self ) instance.transient_data["comp"] = comp self._add_instance_to_context(instance) From 00d9aa216bd8689862a189eb32fcd1dd692303fe Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 12:01:11 +0200 Subject: [PATCH 27/30] Apply to workfile family - like other hosts do --- .../fusion/plugins/publish/increment_current_file_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/increment_current_file_deadline.py b/openpype/hosts/fusion/plugins/publish/increment_current_file_deadline.py index 5c595638e9..42891446f7 100644 --- a/openpype/hosts/fusion/plugins/publish/increment_current_file_deadline.py +++ b/openpype/hosts/fusion/plugins/publish/increment_current_file_deadline.py @@ -11,7 +11,7 @@ class FusionIncrementCurrentFile(pyblish.api.ContextPlugin): label = "Increment current file" order = pyblish.api.IntegratorOrder + 9.0 hosts = ["fusion"] - families = ["render.farm"] + families = ["workfile"] optional = True def process(self, context): From 34c1346961a621726dfc4f9352606c038d650ec4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 12:01:28 +0200 Subject: [PATCH 28/30] Refactor filename --- ...crement_current_file_deadline.py => increment_current_file.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/hosts/fusion/plugins/publish/{increment_current_file_deadline.py => increment_current_file.py} (100%) diff --git a/openpype/hosts/fusion/plugins/publish/increment_current_file_deadline.py b/openpype/hosts/fusion/plugins/publish/increment_current_file.py similarity index 100% rename from openpype/hosts/fusion/plugins/publish/increment_current_file_deadline.py rename to openpype/hosts/fusion/plugins/publish/increment_current_file.py From d6080593e3213df882b5e4b6dc112223531d109e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 12:02:16 +0200 Subject: [PATCH 29/30] Include workfile family --- openpype/hosts/fusion/plugins/publish/save_scene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/save_scene.py b/openpype/hosts/fusion/plugins/publish/save_scene.py index 0cdfafa095..a249c453d8 100644 --- a/openpype/hosts/fusion/plugins/publish/save_scene.py +++ b/openpype/hosts/fusion/plugins/publish/save_scene.py @@ -7,7 +7,7 @@ class FusionSaveComp(pyblish.api.ContextPlugin): label = "Save current file" order = pyblish.api.ExtractorOrder - 0.49 hosts = ["fusion"] - families = ["render"] + families = ["render", "workfile"] def process(self, context): From eed65758e651acf372d6814b4d7061990d6ad993 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 12:21:31 +0200 Subject: [PATCH 30/30] Move Submit Fusion Deadline to Deadline Module --- .../deadline/plugins/publish/submit_fusion_deadline.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/{hosts/fusion/plugins/publish/submit_deadline.py => modules/deadline/plugins/publish/submit_fusion_deadline.py} (100%) diff --git a/openpype/hosts/fusion/plugins/publish/submit_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py similarity index 100% rename from openpype/hosts/fusion/plugins/publish/submit_deadline.py rename to openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py