From e8d6f1d9e9764f90128938ce1386a5b767607c60 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 20 Feb 2022 22:02:56 +0100 Subject: [PATCH 01/58] Pull Fusion integration from `colorbleed` --- openpype/hosts/fusion/__init__.py | 5 + openpype/hosts/fusion/api/menu.py | 57 ++++++--- openpype/hosts/fusion/api/menu_style.qss | 29 ----- .../fusion/deploy/Config/openpype_menu.fu | 60 +++++++++ .../hosts/fusion/deploy/MenuScripts/README.md | 6 + .../deploy/MenuScripts/install_pyside2.py | 29 +++++ .../MenuScripts/openpype_menu.py} | 10 ++ .../32bit/backgrounds_selected_to32bit.py | 0 .../Comp}/32bit/backgrounds_to32bit.py | 0 .../Comp}/32bit/loaders_selected_to32bit.py | 0 .../Scripts/Comp}/32bit/loaders_to32bit.py | 0 .../Scripts/Comp}/switch_ui.py | 0 .../Scripts/Comp}/update_loader_ranges.py | 0 .../hosts/fusion/deploy/fusion_shared.prefs | 19 +++ .../hosts/fusion/hooks/pre_fusion_setup.py | 114 ++++-------------- openpype/hosts/fusion/plugins/load/actions.py | 2 + .../fusion/scripts/fusion_switch_shot.py | 2 +- 17 files changed, 197 insertions(+), 136 deletions(-) delete mode 100644 openpype/hosts/fusion/api/menu_style.qss create mode 100644 openpype/hosts/fusion/deploy/Config/openpype_menu.fu create mode 100644 openpype/hosts/fusion/deploy/MenuScripts/README.md create mode 100644 openpype/hosts/fusion/deploy/MenuScripts/install_pyside2.py rename openpype/hosts/fusion/{utility_scripts/__OpenPype_Menu__.py => deploy/MenuScripts/openpype_menu.py} (54%) rename openpype/hosts/fusion/{utility_scripts => deploy/Scripts/Comp}/32bit/backgrounds_selected_to32bit.py (100%) rename openpype/hosts/fusion/{utility_scripts => deploy/Scripts/Comp}/32bit/backgrounds_to32bit.py (100%) rename openpype/hosts/fusion/{utility_scripts => deploy/Scripts/Comp}/32bit/loaders_selected_to32bit.py (100%) rename openpype/hosts/fusion/{utility_scripts => deploy/Scripts/Comp}/32bit/loaders_to32bit.py (100%) rename openpype/hosts/fusion/{utility_scripts => deploy/Scripts/Comp}/switch_ui.py (100%) rename openpype/hosts/fusion/{utility_scripts => deploy/Scripts/Comp}/update_loader_ranges.py (100%) create mode 100644 openpype/hosts/fusion/deploy/fusion_shared.prefs diff --git a/openpype/hosts/fusion/__init__.py b/openpype/hosts/fusion/__init__.py index e69de29bb2..02befa76e2 100644 --- a/openpype/hosts/fusion/__init__.py +++ b/openpype/hosts/fusion/__init__.py @@ -0,0 +1,5 @@ +import os + +HOST_DIR = os.path.dirname( + os.path.abspath(__file__) +) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 6234322d7f..7799528462 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -3,26 +3,16 @@ import sys from Qt import QtWidgets, QtCore -from openpype import style +from avalon import api from openpype.tools.utils import host_tools +from openpype.style import load_stylesheet from openpype.hosts.fusion.scripts import ( set_rendermode, duplicate_with_inputs ) -def load_stylesheet(): - path = os.path.join(os.path.dirname(__file__), "menu_style.qss") - if not os.path.exists(path): - print("Unable to load stylesheet, file not found in resources") - return "" - - with open(path, "r") as file_stream: - stylesheet = file_stream.read() - return stylesheet - - class Spacer(QtWidgets.QWidget): def __init__(self, height, *args, **kwargs): super(Spacer, self).__init__(*args, **kwargs) @@ -55,6 +45,15 @@ class OpenPypeMenu(QtWidgets.QWidget): ) self.render_mode_widget = None self.setWindowTitle("OpenPype") + + asset_label = QtWidgets.QLabel("Context", self) + asset_label.setStyleSheet("""QLabel { + font-size: 14px; + font-weight: 600; + color: #5f9fb8; + }""") + asset_label.setAlignment(QtCore.Qt.AlignHCenter) + workfiles_btn = QtWidgets.QPushButton("Workfiles...", self) create_btn = QtWidgets.QPushButton("Create...", self) publish_btn = QtWidgets.QPushButton("Publish...", self) @@ -72,10 +71,17 @@ class OpenPypeMenu(QtWidgets.QWidget): layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(10, 20, 10, 20) + layout.addWidget(asset_label) + + layout.addWidget(Spacer(15, self)) + layout.addWidget(workfiles_btn) + + layout.addWidget(Spacer(15, self)) + layout.addWidget(create_btn) - layout.addWidget(publish_btn) layout.addWidget(load_btn) + layout.addWidget(publish_btn) layout.addWidget(manager_btn) layout.addWidget(Spacer(15, self)) @@ -93,6 +99,9 @@ class OpenPypeMenu(QtWidgets.QWidget): self.setLayout(layout) + # Store reference so we can update the label + 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) @@ -104,6 +113,26 @@ class OpenPypeMenu(QtWidgets.QWidget): self.on_duplicate_with_inputs_clicked) reset_resolution_btn.clicked.connect(self.on_reset_resolution_clicked) + self._callbacks = [] + self.register_callback("taskChanged", self.on_task_changed) + self.on_task_changed() + + def on_task_changed(self): + # Update current context label + label = api.Session["AVALON_ASSET"] + self.asset_label.setText(label) + + def register_callback(self, name, fn): + + # Create a wrapper callback that we only store + # for as long as we want it to persist as callback + callback = lambda *args: fn() + self._callbacks.append(callback) + api.on(name, callback) + + def deregister_all_callbacks(self): + self._callbacks[:] = [] + def on_workfile_clicked(self): print("Clicked Workfile") host_tools.show_workfiles() @@ -132,7 +161,7 @@ class OpenPypeMenu(QtWidgets.QWidget): print("Clicked Set Render Mode") if self.render_mode_widget is None: window = set_rendermode.SetRenderMode() - window.setStyleSheet(style.load_stylesheet()) + window.setStyleSheet(load_stylesheet()) window.show() self.render_mode_widget = window else: diff --git a/openpype/hosts/fusion/api/menu_style.qss b/openpype/hosts/fusion/api/menu_style.qss deleted file mode 100644 index 12c474b070..0000000000 --- a/openpype/hosts/fusion/api/menu_style.qss +++ /dev/null @@ -1,29 +0,0 @@ -QWidget { - background-color: #282828; - border-radius: 3; -} - -QPushButton { - border: 1px solid #090909; - background-color: #201f1f; - color: #ffffff; - padding: 5; -} - -QPushButton:focus { - background-color: "#171717"; - color: #d0d0d0; -} - -QPushButton:hover { - background-color: "#171717"; - color: #e64b3d; -} - -#OpenPypeMenu { - border: 1px solid #fef9ef; -} - -#Spacer { - background-color: #282828; -} diff --git a/openpype/hosts/fusion/deploy/Config/openpype_menu.fu b/openpype/hosts/fusion/deploy/Config/openpype_menu.fu new file mode 100644 index 0000000000..8b8d448259 --- /dev/null +++ b/openpype/hosts/fusion/deploy/Config/openpype_menu.fu @@ -0,0 +1,60 @@ +{ + Action + { + ID = "OpenPype_Menu", + Category = "OpenPype", + Name = "OpenPype Menu", + + Targets = + { + Composition = + { + Execute = _Lua [=[ + local scriptPath = app:MapPath("OpenPype:MenuScripts/openpype_menu.py") + if bmd.fileexists(scriptPath) == false then + print("[OpenPype Error] Can't run file: " .. scriptPath) + else + target:RunScript(scriptPath) + end + ]=], + }, + }, + }, + Action + { + ID = "OpenPype_Install_PySide2", + Category = "OpenPype", + Name = "Install PySide2", + + Targets = + { + Composition = + { + Execute = _Lua [=[ + local scriptPath = app:MapPath("OpenPype:MenuScripts/install_pyside2.py") + if bmd.fileexists(scriptPath) == false then + print("[OpenPype Error] Can't run file: " .. scriptPath) + else + target:RunScript(scriptPath) + end + ]=], + }, + }, + }, + Menus + { + Target = "ChildFrame", + + Before "Help" + { + Sub "OpenPype" + { + "OpenPype_Menu{}", + "_", + Sub "Admin" { + "OpenPype_Install_PySide2{}" + } + } + }, + }, +} diff --git a/openpype/hosts/fusion/deploy/MenuScripts/README.md b/openpype/hosts/fusion/deploy/MenuScripts/README.md new file mode 100644 index 0000000000..f87eaea4a2 --- /dev/null +++ b/openpype/hosts/fusion/deploy/MenuScripts/README.md @@ -0,0 +1,6 @@ +### OpenPype deploy MenuScripts + +Note that this `MenuScripts` is not an official Fusion folder. +OpenPype only uses this folder in `{fusion}/deploy/` to trigger the OpenPype menu actions. + +They are used in the actions defined in `.fu` files in `{fusion}/deploy/Config`. \ No newline at end of file diff --git a/openpype/hosts/fusion/deploy/MenuScripts/install_pyside2.py b/openpype/hosts/fusion/deploy/MenuScripts/install_pyside2.py new file mode 100644 index 0000000000..4fcdb7658f --- /dev/null +++ b/openpype/hosts/fusion/deploy/MenuScripts/install_pyside2.py @@ -0,0 +1,29 @@ +# This is just a quick hack for users running Py3 locally but having no +# Qt library installed +import os +import subprocess +import importlib + + +try: + from Qt import QtWidgets + from Qt import __binding__ + print(f"Qt binding: {__binding__}") + mod = importlib.import_module(__binding__) + print(f"Qt path: {mod.__file__}") + print("Qt library found, nothing to do..") + +except ImportError as exc: + print("Assuming no Qt library is installed..") + print('Installing PySide2 for Python 3.6: ' + f'{os.environ["FUSION16_PYTHON36_HOME"]}') + + # Get full path to python executable + exe = "python.exe" if os.name == 'nt' else "python" + python = os.path.join(os.environ["FUSION16_PYTHON36_HOME"], exe) + assert os.path.exists(python), f"Python doesn't exist: {python}" + + # Do python -m pip install PySide2 + args = [python, "-m", "pip", "install", "PySide2"] + print(f"Args: {args}") + subprocess.Popen(args) diff --git a/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py b/openpype/hosts/fusion/deploy/MenuScripts/openpype_menu.py similarity index 54% rename from openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py rename to openpype/hosts/fusion/deploy/MenuScripts/openpype_menu.py index 4b5e8f91a0..20547b21f2 100644 --- a/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py +++ b/openpype/hosts/fusion/deploy/MenuScripts/openpype_menu.py @@ -8,6 +8,11 @@ log = Logger().get_logger(__name__) def main(env): + # This script working directory starts in Fusion application folder. + # 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("..") + import avalon.api from openpype.hosts.fusion import api from openpype.hosts.fusion.api import menu @@ -22,6 +27,11 @@ def main(env): menu.launch_openpype_menu() + # Initiate a QTimer to check if Fusion is still alive every X interval + # If Fusion is not found - kill itself + # todo(roy): Implement timer that ensures UI doesn't remain when e.g. + # Fusion closes down + if __name__ == "__main__": result = main(os.environ) diff --git a/openpype/hosts/fusion/utility_scripts/32bit/backgrounds_selected_to32bit.py b/openpype/hosts/fusion/deploy/Scripts/Comp/32bit/backgrounds_selected_to32bit.py similarity index 100% rename from openpype/hosts/fusion/utility_scripts/32bit/backgrounds_selected_to32bit.py rename to openpype/hosts/fusion/deploy/Scripts/Comp/32bit/backgrounds_selected_to32bit.py diff --git a/openpype/hosts/fusion/utility_scripts/32bit/backgrounds_to32bit.py b/openpype/hosts/fusion/deploy/Scripts/Comp/32bit/backgrounds_to32bit.py similarity index 100% rename from openpype/hosts/fusion/utility_scripts/32bit/backgrounds_to32bit.py rename to openpype/hosts/fusion/deploy/Scripts/Comp/32bit/backgrounds_to32bit.py diff --git a/openpype/hosts/fusion/utility_scripts/32bit/loaders_selected_to32bit.py b/openpype/hosts/fusion/deploy/Scripts/Comp/32bit/loaders_selected_to32bit.py similarity index 100% rename from openpype/hosts/fusion/utility_scripts/32bit/loaders_selected_to32bit.py rename to openpype/hosts/fusion/deploy/Scripts/Comp/32bit/loaders_selected_to32bit.py diff --git a/openpype/hosts/fusion/utility_scripts/32bit/loaders_to32bit.py b/openpype/hosts/fusion/deploy/Scripts/Comp/32bit/loaders_to32bit.py similarity index 100% rename from openpype/hosts/fusion/utility_scripts/32bit/loaders_to32bit.py rename to openpype/hosts/fusion/deploy/Scripts/Comp/32bit/loaders_to32bit.py diff --git a/openpype/hosts/fusion/utility_scripts/switch_ui.py b/openpype/hosts/fusion/deploy/Scripts/Comp/switch_ui.py similarity index 100% rename from openpype/hosts/fusion/utility_scripts/switch_ui.py rename to openpype/hosts/fusion/deploy/Scripts/Comp/switch_ui.py diff --git a/openpype/hosts/fusion/utility_scripts/update_loader_ranges.py b/openpype/hosts/fusion/deploy/Scripts/Comp/update_loader_ranges.py similarity index 100% rename from openpype/hosts/fusion/utility_scripts/update_loader_ranges.py rename to openpype/hosts/fusion/deploy/Scripts/Comp/update_loader_ranges.py diff --git a/openpype/hosts/fusion/deploy/fusion_shared.prefs b/openpype/hosts/fusion/deploy/fusion_shared.prefs new file mode 100644 index 0000000000..998c6a6d66 --- /dev/null +++ b/openpype/hosts/fusion/deploy/fusion_shared.prefs @@ -0,0 +1,19 @@ +{ +Locked = true, +Global = { + Paths = { + Map = { + ["OpenPype:"] = "$(OPENPYPE_FUSION)/deploy", + ["Reactor:"] = "$(REACTOR)", + + ["Config:"] = "UserPaths:Config;OpenPype:Config", + ["Scripts:"] = "UserPaths:Scripts;Reactor:System/Scripts;OpenPype:Scripts", + ["UserPaths:"] = "UserData:;AllData:;Fusion:;Reactor:Deploy" + }, + }, + Script = { + PythonVersion = 3, + Python3Forced = true + }, + }, +} \ No newline at end of file diff --git a/openpype/hosts/fusion/hooks/pre_fusion_setup.py b/openpype/hosts/fusion/hooks/pre_fusion_setup.py index e635a0ea74..c78d433e5c 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_setup.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_setup.py @@ -1,8 +1,6 @@ import os -import shutil - -import openpype.hosts.fusion from openpype.lib import PreLaunchHook, ApplicationLaunchFailed +from openpype.hosts.fusion import HOST_DIR class FusionPrelaunch(PreLaunchHook): @@ -14,101 +12,33 @@ class FusionPrelaunch(PreLaunchHook): def execute(self): # making sure python 3.6 is installed at provided path - py36_dir = self.launch_context.env.get("PYTHON36") - if not py36_dir: - raise ApplicationLaunchFailed( - "Required environment variable \"PYTHON36\" is not set." - "\n\nFusion implementation requires to have" - " installed Python 3.6" - ) + py36_var = "FUSION16_PYTHON36_HOME" + fusion_python36_home = self.launch_context.env.get(py36_var, "") - py36_dir = os.path.normpath(py36_dir) - if not os.path.isdir(py36_dir): + self.log.info(f"Looking for Python 3.6 in: {fusion_python36_home}") + for path in fusion_python36_home.split(os.pathsep): + # Allow defining multiple paths to allow "fallback" to other + # path. But make to set only a single path as final variable. + py36_dir = os.path.normpath(path) + if os.path.isdir(py36_dir): + break + else: raise ApplicationLaunchFailed( "Python 3.6 is not installed at the provided path.\n" "Either make sure the environments in fusion settings has" " 'PYTHON36' set corectly or make sure Python 3.6 is installed" - f" in the given path.\n\nPYTHON36: {py36_dir}" - ) - self.log.info(f"Path to Fusion Python folder: '{py36_dir}'...") - self.launch_context.env["PYTHON36"] = py36_dir - - utility_dir = self.launch_context.env.get("FUSION_UTILITY_SCRIPTS_DIR") - if not utility_dir: - raise ApplicationLaunchFailed( - "Required Fusion utility script dir environment variable" - " \"FUSION_UTILITY_SCRIPTS_DIR\" is not set." + f" in the given path.\n\nPYTHON36: {fusion_python36_home}" ) - # setting utility scripts dir for scripts syncing - utility_dir = os.path.normpath(utility_dir) - if not os.path.isdir(utility_dir): - raise ApplicationLaunchFailed( - "Fusion utility script dir does not exist. Either make sure " - "the environments in fusion settings has" - " 'FUSION_UTILITY_SCRIPTS_DIR' set correctly or reinstall " - f"Fusion.\n\nFUSION_UTILITY_SCRIPTS_DIR: '{utility_dir}'" - ) + self.log.info(f"Setting {py36_var}: '{py36_dir}'...") + self.launch_context.env[py36_var] = py36_dir - self._sync_utility_scripts(self.launch_context.env) - self.log.info("Fusion Pype wrapper has been installed") + # Add our Fusion Master Prefs which is the only way to customize + # Fusion to define where it can read custom scripts and tools from + self.log.info(f"Setting OPENPYPE_FUSION: {HOST_DIR}") + self.launch_context.env["OPENPYPE_FUSION"] = HOST_DIR - def _sync_utility_scripts(self, env): - """ Synchronizing basic utlility scripts for resolve. - - To be able to run scripts from inside `Fusion/Workspace/Scripts` menu - all scripts has to be accessible from defined folder. - """ - if not env: - env = {k: v for k, v in os.environ.items()} - - # initiate inputs - scripts = {} - us_env = env.get("FUSION_UTILITY_SCRIPTS_SOURCE_DIR") - us_dir = env.get("FUSION_UTILITY_SCRIPTS_DIR", "") - us_paths = [os.path.join( - os.path.dirname(os.path.abspath(openpype.hosts.fusion.__file__)), - "utility_scripts" - )] - - # collect script dirs - if us_env: - self.log.info(f"Utility Scripts Env: `{us_env}`") - us_paths = us_env.split( - os.pathsep) + us_paths - - # collect scripts from dirs - for path in us_paths: - scripts.update({path: os.listdir(path)}) - - self.log.info(f"Utility Scripts Dir: `{us_paths}`") - self.log.info(f"Utility Scripts: `{scripts}`") - - # make sure no script file is in folder - if next((s for s in os.listdir(us_dir)), None): - for s in os.listdir(us_dir): - path = os.path.normpath( - os.path.join(us_dir, s)) - self.log.info(f"Removing `{path}`...") - - # remove file or directory if not in our folders - if not os.path.isdir(path): - os.remove(path) - else: - shutil.rmtree(path) - - # copy scripts into Resolve's utility scripts dir - for d, sl in scripts.items(): - # directory and scripts list - for s in sl: - # script in script list - src = os.path.normpath(os.path.join(d, s)) - dst = os.path.normpath(os.path.join(us_dir, s)) - - self.log.info(f"Copying `{src}` to `{dst}`...") - - # copy file or directory from our folders to fusion's folder - if not os.path.isdir(src): - shutil.copy2(src, dst) - else: - shutil.copytree(src, dst) + pref_var = "FUSION16_MasterPrefs" # used by both Fu16 and Fu17 + prefs = os.path.join(HOST_DIR, "deploy", "fusion_shared.prefs") + self.log.info(f"Setting {pref_var}: {prefs}") + self.launch_context.env[pref_var] = prefs diff --git a/openpype/hosts/fusion/plugins/load/actions.py b/openpype/hosts/fusion/plugins/load/actions.py index 6af99e4c56..84b66fc69a 100644 --- a/openpype/hosts/fusion/plugins/load/actions.py +++ b/openpype/hosts/fusion/plugins/load/actions.py @@ -11,6 +11,7 @@ class FusionSetFrameRangeLoader(api.Loader): families = ["animation", "camera", "imagesequence", + "render", "yeticache", "pointcache", "render"] @@ -45,6 +46,7 @@ class FusionSetFrameRangeWithHandlesLoader(api.Loader): families = ["animation", "camera", "imagesequence", + "render", "yeticache", "pointcache", "render"] diff --git a/openpype/hosts/fusion/scripts/fusion_switch_shot.py b/openpype/hosts/fusion/scripts/fusion_switch_shot.py index 041b53f6c9..9dd8a351e4 100644 --- a/openpype/hosts/fusion/scripts/fusion_switch_shot.py +++ b/openpype/hosts/fusion/scripts/fusion_switch_shot.py @@ -176,7 +176,7 @@ def update_frame_range(comp, representations): versions = list(versions) versions = [v for v in versions - if v["data"].get("startFrame", None) is not None] + if v["data"].get("frameStart", None) is not None] if not versions: log.warning("No versions loaded to match frame range to.\n") From e72c7680684172c399b1a07f346c9897ba75d508 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 17 Feb 2022 12:30:10 +0100 Subject: [PATCH 02/58] fusion: adding reset resolution (cherry picked from commit 209511ee3d938e21c20cff03edcdbbde4a4791f0) --- openpype/hosts/fusion/api/__init__.py | 4 +++- openpype/hosts/fusion/api/lib.py | 29 +++++++++++++++++++++++++-- openpype/hosts/fusion/api/menu.py | 23 +++++++++++++-------- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/fusion/api/__init__.py b/openpype/hosts/fusion/api/__init__.py index 19d1e092fe..78afabdb45 100644 --- a/openpype/hosts/fusion/api/__init__.py +++ b/openpype/hosts/fusion/api/__init__.py @@ -23,7 +23,8 @@ from .workio import ( from .lib import ( maintained_selection, get_additional_data, - update_frame_range + update_frame_range, + set_framerange ) from .menu import launch_openpype_menu @@ -53,6 +54,7 @@ __all__ = [ "maintained_selection", "get_additional_data", "update_frame_range", + "set_framerange", # menu "launch_openpype_menu", diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 5d97f83032..37a13e4a10 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -8,12 +8,14 @@ from Qt import QtGui import avalon.api from avalon import io from .pipeline import get_current_comp, comp_lock_and_undo_chunk - +from openpype.api import ( + get_asset +) self = sys.modules[__name__] self._project = None -def update_frame_range(start, end, comp=None, set_render_range=True): +def update_frame_range(start, end, comp=None, set_render_range=True, **kwargs): """Set Fusion comp's start and end frame range Args: @@ -22,6 +24,7 @@ def update_frame_range(start, end, comp=None, set_render_range=True): comp (object, Optional): comp object from fusion set_render_range (bool, Optional): When True this will also set the composition's render start and end frame. + kwargs (dict): additional kwargs Returns: None @@ -36,6 +39,16 @@ def update_frame_range(start, end, comp=None, set_render_range=True): "COMPN_GlobalEnd": end } + # exclude handles if any found in kwargs + if kwargs.get("handle_start"): + handle_start = kwargs.get("handle_start") + attrs["COMPN_GlobalStart"] = int(start - handle_start) + + if kwargs.get("handle_end"): + handle_end = kwargs.get("handle_end") + attrs["COMPN_GlobalEnd"] = int(end + handle_end) + + # set frame range if set_render_range: attrs.update({ "COMPN_RenderStart": start, @@ -46,6 +59,18 @@ def update_frame_range(start, end, comp=None, set_render_range=True): comp.SetAttrs(attrs) +def set_framerange(): + asset_doc = get_asset() + start = asset_doc["data"]["frameStart"] + end = asset_doc["data"]["frameEnd"] + + data = { + "handle_start": asset_doc["data"]["handleStart"], + "handle_end": asset_doc["data"]["handleEnd"] + } + update_frame_range(start, end, set_render_range=True, **data) + + def get_additional_data(container): """Get Fusion related data for the container diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 7799528462..31a3b5b88c 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -1,4 +1,3 @@ -import os import sys from Qt import QtWidgets, QtCore @@ -11,7 +10,9 @@ from openpype.hosts.fusion.scripts import ( set_rendermode, duplicate_with_inputs ) - +from openpype.hosts.fusion.api import ( + set_framerange +) class Spacer(QtWidgets.QWidget): def __init__(self, height, *args, **kwargs): @@ -61,12 +62,11 @@ class OpenPypeMenu(QtWidgets.QWidget): manager_btn = QtWidgets.QPushButton("Manage...", self) libload_btn = QtWidgets.QPushButton("Library...", self) rendermode_btn = QtWidgets.QPushButton("Set render mode...", self) + set_framerange_btn = QtWidgets.QPushButton("Set Frame Range", self) + set_resolution_btn = QtWidgets.QPushButton("Set Resolution", self) duplicate_with_inputs_btn = QtWidgets.QPushButton( "Duplicate with input connections", self ) - reset_resolution_btn = QtWidgets.QPushButton( - "Reset Resolution from project", self - ) layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(10, 20, 10, 20) @@ -90,12 +90,13 @@ class OpenPypeMenu(QtWidgets.QWidget): layout.addWidget(Spacer(15, self)) + layout.addWidget(set_framerange_btn) + layout.addWidget(set_resolution_btn) layout.addWidget(rendermode_btn) layout.addWidget(Spacer(15, self)) layout.addWidget(duplicate_with_inputs_btn) - layout.addWidget(reset_resolution_btn) self.setLayout(layout) @@ -111,7 +112,8 @@ class OpenPypeMenu(QtWidgets.QWidget): rendermode_btn.clicked.connect(self.on_rendernode_clicked) duplicate_with_inputs_btn.clicked.connect( self.on_duplicate_with_inputs_clicked) - reset_resolution_btn.clicked.connect(self.on_reset_resolution_clicked) + set_resolution_btn.clicked.connect(self.on_set_resolution_clicked) + set_framerange_btn.clicked.connect(self.on_set_framerange_clicked) self._callbacks = [] self.register_callback("taskChanged", self.on_task_changed) @@ -171,9 +173,14 @@ class OpenPypeMenu(QtWidgets.QWidget): duplicate_with_inputs.duplicate_with_input_connections() print("Clicked Set Colorspace") - def on_reset_resolution_clicked(self): + def on_set_resolution_clicked(self): print("Clicked Reset Resolution") + def on_set_framerange_clicked(self): + print("Clicked Reset Framerange") + set_framerange() + + def launch_openpype_menu(): app = QtWidgets.QApplication(sys.argv) From 1b40d5102fac1f726a03a6f5d08fcd3a519f16c2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 20 Feb 2022 22:22:50 +0100 Subject: [PATCH 03/58] Fix hound issues --- openpype/hosts/fusion/deploy/MenuScripts/install_pyside2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/deploy/MenuScripts/install_pyside2.py b/openpype/hosts/fusion/deploy/MenuScripts/install_pyside2.py index 4fcdb7658f..ab9f13ce05 100644 --- a/openpype/hosts/fusion/deploy/MenuScripts/install_pyside2.py +++ b/openpype/hosts/fusion/deploy/MenuScripts/install_pyside2.py @@ -6,14 +6,14 @@ import importlib try: - from Qt import QtWidgets + from Qt import QtWidgets # noqa: F401 from Qt import __binding__ print(f"Qt binding: {__binding__}") mod = importlib.import_module(__binding__) print(f"Qt path: {mod.__file__}") print("Qt library found, nothing to do..") -except ImportError as exc: +except ImportError: print("Assuming no Qt library is installed..") print('Installing PySide2 for Python 3.6: ' f'{os.environ["FUSION16_PYTHON36_HOME"]}') From 43be2004c7e14a272aa512df9bcc4916c5dac8a4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 20 Feb 2022 22:24:42 +0100 Subject: [PATCH 04/58] Avoid assigning a lambda expression --- openpype/hosts/fusion/api/menu.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 31a3b5b88c..1d7e1092da 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -14,6 +14,7 @@ from openpype.hosts.fusion.api import ( set_framerange ) + class Spacer(QtWidgets.QWidget): def __init__(self, height, *args, **kwargs): super(Spacer, self).__init__(*args, **kwargs) @@ -128,9 +129,11 @@ class OpenPypeMenu(QtWidgets.QWidget): # Create a wrapper callback that we only store # for as long as we want it to persist as callback - callback = lambda *args: fn() - self._callbacks.append(callback) - api.on(name, callback) + def _callback(*args): + fn() + + self._callbacks.append(_callback) + api.on(name, _callback) def deregister_all_callbacks(self): self._callbacks[:] = [] From 860f159ec369fedd423181ce0342d9b19c91db8f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 20 Feb 2022 22:25:59 +0100 Subject: [PATCH 05/58] Fix double "render" family --- openpype/hosts/fusion/plugins/load/actions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/load/actions.py b/openpype/hosts/fusion/plugins/load/actions.py index 84b66fc69a..6af99e4c56 100644 --- a/openpype/hosts/fusion/plugins/load/actions.py +++ b/openpype/hosts/fusion/plugins/load/actions.py @@ -11,7 +11,6 @@ class FusionSetFrameRangeLoader(api.Loader): families = ["animation", "camera", "imagesequence", - "render", "yeticache", "pointcache", "render"] @@ -46,7 +45,6 @@ class FusionSetFrameRangeWithHandlesLoader(api.Loader): families = ["animation", "camera", "imagesequence", - "render", "yeticache", "pointcache", "render"] From cdca18e93f53ba1e222d52d90fecebf0e7feff01 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 22 Feb 2022 20:58:34 +0100 Subject: [PATCH 06/58] Fusion: Add support for open last workfile --- openpype/hooks/pre_add_last_workfile_arg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index 653f97b3dd..eb9e6a6b1c 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -18,6 +18,7 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): "nukex", "hiero", "nukestudio", + "fusion", "blender", "photoshop", "tvpaint", From 4e614c8337be7ea35ce4a063385d844d90c1e896 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 22 Feb 2022 21:02:43 +0100 Subject: [PATCH 07/58] Fix typo + wrong label --- openpype/hosts/fusion/api/menu.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 1d7e1092da..7b23e2bc2b 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -110,7 +110,7 @@ class OpenPypeMenu(QtWidgets.QWidget): load_btn.clicked.connect(self.on_load_clicked) manager_btn.clicked.connect(self.on_manager_clicked) libload_btn.clicked.connect(self.on_libload_clicked) - rendermode_btn.clicked.connect(self.on_rendernode_clicked) + rendermode_btn.clicked.connect(self.on_rendermode_clicked) duplicate_with_inputs_btn.clicked.connect( self.on_duplicate_with_inputs_clicked) set_resolution_btn.clicked.connect(self.on_set_resolution_clicked) @@ -162,7 +162,7 @@ class OpenPypeMenu(QtWidgets.QWidget): print("Clicked Library") host_tools.show_library_loader() - def on_rendernode_clicked(self): + def on_rendermode_clicked(self): print("Clicked Set Render Mode") if self.render_mode_widget is None: window = set_rendermode.SetRenderMode() @@ -173,8 +173,8 @@ class OpenPypeMenu(QtWidgets.QWidget): self.render_mode_widget.show() def on_duplicate_with_inputs_clicked(self): + print("Clicked Duplicate with input connections") duplicate_with_inputs.duplicate_with_input_connections() - print("Clicked Set Colorspace") def on_set_resolution_clicked(self): print("Clicked Reset Resolution") From 959aa2a47974075d97be41dc5c49fb6ee2641875 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 22 Feb 2022 21:10:30 +0100 Subject: [PATCH 08/58] Remove duplicate script that has more recent version in openpype/hosts/fusion/scripts --- openpype/scripts/fusion_switch_shot.py | 248 ------------------------- 1 file changed, 248 deletions(-) delete mode 100644 openpype/scripts/fusion_switch_shot.py diff --git a/openpype/scripts/fusion_switch_shot.py b/openpype/scripts/fusion_switch_shot.py deleted file mode 100644 index 26f5356336..0000000000 --- a/openpype/scripts/fusion_switch_shot.py +++ /dev/null @@ -1,248 +0,0 @@ -import os -import re -import sys -import logging - -# Pipeline imports -from avalon import api, io, pipeline -import avalon.fusion - -# Config imports -import openpype.lib as pype -import openpype.hosts.fusion.lib as fusion_lib - -log = logging.getLogger("Update Slap Comp") - -self = sys.modules[__name__] -self._project = None - - -def _format_version_folder(folder): - """Format a version folder based on the filepath - - Assumption here is made that, if the path does not exists the folder - will be "v001" - - Args: - folder: file path to a folder - - Returns: - str: new version folder name - """ - - new_version = 1 - if os.path.isdir(folder): - re_version = re.compile("v\d+$") - versions = [i for i in os.listdir(folder) if os.path.isdir(i) - and re_version.match(i)] - if versions: - # ensure the "v" is not included - new_version = int(max(versions)[1:]) + 1 - - version_folder = "v{:03d}".format(new_version) - - return version_folder - - -def _get_work_folder(session): - """Convenience function to get the work folder path of the current asset""" - - # Get new filename, create path based on asset and work template - template_work = self._project["config"]["template"]["work"] - work_path = pipeline._format_work_template(template_work, session) - - return os.path.normpath(work_path) - - -def _get_fusion_instance(): - fusion = getattr(sys.modules["__main__"], "fusion", None) - if fusion is None: - try: - # Support for FuScript.exe, BlackmagicFusion module for py2 only - import BlackmagicFusion as bmf - fusion = bmf.scriptapp("Fusion") - except ImportError: - raise RuntimeError("Could not find a Fusion instance") - return fusion - - -def _format_filepath(session): - - project = session["AVALON_PROJECT"] - asset = session["AVALON_ASSET"] - - # Save updated slap comp - work_path = _get_work_folder(session) - walk_to_dir = os.path.join(work_path, "scenes", "slapcomp") - slapcomp_dir = os.path.abspath(walk_to_dir) - - # Ensure destination exists - if not os.path.isdir(slapcomp_dir): - log.warning("Folder did not exist, creating folder structure") - os.makedirs(slapcomp_dir) - - # Compute output path - new_filename = "{}_{}_slapcomp_v001.comp".format(project, asset) - new_filepath = os.path.join(slapcomp_dir, new_filename) - - # Create new unqiue filepath - if os.path.exists(new_filepath): - new_filepath = pype.version_up(new_filepath) - - return new_filepath - - -def _update_savers(comp, session): - """Update all savers of the current comp to ensure the output is correct - - Args: - comp (object): current comp instance - session (dict): the current Avalon session - - Returns: - None - """ - - new_work = _get_work_folder(session) - renders = os.path.join(new_work, "renders") - version_folder = _format_version_folder(renders) - renders_version = os.path.join(renders, version_folder) - - comp.Print("New renders to: %s\n" % renders) - - with avalon.fusion.comp_lock_and_undo_chunk(comp): - savers = comp.GetToolList(False, "Saver").values() - for saver in savers: - filepath = saver.GetAttrs("TOOLST_Clip_Name")[1.0] - filename = os.path.basename(filepath) - new_path = os.path.join(renders_version, filename) - saver["Clip"] = new_path - - -def update_frame_range(comp, representations): - """Update the frame range of the comp and render length - - The start and end frame are based on the lowest start frame and the highest - end frame - - Args: - comp (object): current focused comp - representations (list) collection of dicts - - Returns: - None - - """ - - version_ids = [r["parent"] for r in representations] - versions = io.find({"type": "version", "_id": {"$in": version_ids}}) - versions = list(versions) - - start = min(v["data"]["frameStart"] for v in versions) - end = max(v["data"]["frameEnd"] for v in versions) - - fusion_lib.update_frame_range(start, end, comp=comp) - - -def switch(asset_name, filepath=None, new=True): - """Switch the current containers of the file to the other asset (shot) - - Args: - filepath (str): file path of the comp file - asset_name (str): name of the asset (shot) - new (bool): Save updated comp under a different name - - Returns: - comp path (str): new filepath of the updated comp - - """ - - # If filepath provided, ensure it is valid absolute path - if filepath is not None: - if not os.path.isabs(filepath): - filepath = os.path.abspath(filepath) - - assert os.path.exists(filepath), "%s must exist " % filepath - - # Assert asset name exists - # It is better to do this here then to wait till switch_shot does it - asset = io.find_one({"type": "asset", "name": asset_name}) - assert asset, "Could not find '%s' in the database" % asset_name - - # Get current project - self._project = io.find_one({ - "type": "project", - "name": api.Session["AVALON_PROJECT"] - }) - - # Go to comp - if not filepath: - current_comp = avalon.fusion.get_current_comp() - assert current_comp is not None, "Could not find current comp" - else: - fusion = _get_fusion_instance() - current_comp = fusion.LoadComp(filepath, quiet=True) - assert current_comp is not None, "Fusion could not load '%s'" % filepath - - host = api.registered_host() - containers = list(host.ls()) - assert containers, "Nothing to update" - - representations = [] - for container in containers: - try: - representation = fusion_lib.switch_item(container, - asset_name=asset_name) - representations.append(representation) - except Exception as e: - current_comp.Print("Error in switching! %s\n" % e.message) - - message = "Switched %i Loaders of the %i\n" % (len(representations), - len(containers)) - current_comp.Print(message) - - # Build the session to switch to - switch_to_session = api.Session.copy() - switch_to_session["AVALON_ASSET"] = asset['name'] - - if new: - comp_path = _format_filepath(switch_to_session) - - # Update savers output based on new session - _update_savers(current_comp, switch_to_session) - else: - comp_path = pype.version_up(filepath) - - current_comp.Print(comp_path) - - current_comp.Print("\nUpdating frame range") - update_frame_range(current_comp, representations) - - current_comp.Save(comp_path) - - return comp_path - - -if __name__ == '__main__': - - import argparse - - parser = argparse.ArgumentParser(description="Switch to a shot within an" - "existing comp file") - - parser.add_argument("--file_path", - type=str, - default=True, - help="File path of the comp to use") - - parser.add_argument("--asset_name", - type=str, - default=True, - help="Name of the asset (shot) to switch") - - args, unknown = parser.parse_args() - - api.install(avalon.fusion) - switch(args.asset_name, args.file_path) - - sys.exit(0) From 51ef4ce4e5de954bde31ac634ecb1553b7ceee62 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 22 Feb 2022 21:11:40 +0100 Subject: [PATCH 09/58] Move Comp scripts to an OpenPype submenu to clarify those are OpenPype scripts --- .../Comp/{ => OpenPype}/32bit/backgrounds_selected_to32bit.py | 0 .../Scripts/Comp/{ => OpenPype}/32bit/backgrounds_to32bit.py | 0 .../Scripts/Comp/{ => OpenPype}/32bit/loaders_selected_to32bit.py | 0 .../deploy/Scripts/Comp/{ => OpenPype}/32bit/loaders_to32bit.py | 0 .../hosts/fusion/deploy/Scripts/Comp/{ => OpenPype}/switch_ui.py | 0 .../deploy/Scripts/Comp/{ => OpenPype}/update_loader_ranges.py | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename openpype/hosts/fusion/deploy/Scripts/Comp/{ => OpenPype}/32bit/backgrounds_selected_to32bit.py (100%) rename openpype/hosts/fusion/deploy/Scripts/Comp/{ => OpenPype}/32bit/backgrounds_to32bit.py (100%) rename openpype/hosts/fusion/deploy/Scripts/Comp/{ => OpenPype}/32bit/loaders_selected_to32bit.py (100%) rename openpype/hosts/fusion/deploy/Scripts/Comp/{ => OpenPype}/32bit/loaders_to32bit.py (100%) rename openpype/hosts/fusion/deploy/Scripts/Comp/{ => OpenPype}/switch_ui.py (100%) rename openpype/hosts/fusion/deploy/Scripts/Comp/{ => OpenPype}/update_loader_ranges.py (100%) diff --git a/openpype/hosts/fusion/deploy/Scripts/Comp/32bit/backgrounds_selected_to32bit.py b/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/backgrounds_selected_to32bit.py similarity index 100% rename from openpype/hosts/fusion/deploy/Scripts/Comp/32bit/backgrounds_selected_to32bit.py rename to openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/backgrounds_selected_to32bit.py diff --git a/openpype/hosts/fusion/deploy/Scripts/Comp/32bit/backgrounds_to32bit.py b/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/backgrounds_to32bit.py similarity index 100% rename from openpype/hosts/fusion/deploy/Scripts/Comp/32bit/backgrounds_to32bit.py rename to openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/backgrounds_to32bit.py diff --git a/openpype/hosts/fusion/deploy/Scripts/Comp/32bit/loaders_selected_to32bit.py b/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/loaders_selected_to32bit.py similarity index 100% rename from openpype/hosts/fusion/deploy/Scripts/Comp/32bit/loaders_selected_to32bit.py rename to openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/loaders_selected_to32bit.py diff --git a/openpype/hosts/fusion/deploy/Scripts/Comp/32bit/loaders_to32bit.py b/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/loaders_to32bit.py similarity index 100% rename from openpype/hosts/fusion/deploy/Scripts/Comp/32bit/loaders_to32bit.py rename to openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/loaders_to32bit.py diff --git a/openpype/hosts/fusion/deploy/Scripts/Comp/switch_ui.py b/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/switch_ui.py similarity index 100% rename from openpype/hosts/fusion/deploy/Scripts/Comp/switch_ui.py rename to openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/switch_ui.py diff --git a/openpype/hosts/fusion/deploy/Scripts/Comp/update_loader_ranges.py b/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/update_loader_ranges.py similarity index 100% rename from openpype/hosts/fusion/deploy/Scripts/Comp/update_loader_ranges.py rename to openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/update_loader_ranges.py From 2f9eb8fa640226aafbe2729b5417d1db016cc56d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 22 Feb 2022 21:32:07 +0100 Subject: [PATCH 10/58] Fix on_pyblish_instance_toggled arguments --- openpype/hosts/fusion/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 64dda0bc8a..0c1c5a4362 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -97,7 +97,7 @@ def uninstall(): ) -def on_pyblish_instance_toggled(instance, new_value, old_value): +def on_pyblish_instance_toggled(instance, old_value, new_value): """Toggle saver tool passthrough states on instance toggles.""" comp = instance.context.data.get("currentComp") if not comp: From 45391662f7a6acc33b4d618f8a7453c1186cdd89 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 17 Mar 2022 12:21:12 +0100 Subject: [PATCH 11/58] Fix merge conflict and remove fusion_switch_shot.py again --- .../fusion/scripts/fusion_switch_shot.py | 285 ------------------ 1 file changed, 285 deletions(-) delete mode 100644 openpype/hosts/fusion/scripts/fusion_switch_shot.py diff --git a/openpype/hosts/fusion/scripts/fusion_switch_shot.py b/openpype/hosts/fusion/scripts/fusion_switch_shot.py deleted file mode 100644 index ca7efb9136..0000000000 --- a/openpype/hosts/fusion/scripts/fusion_switch_shot.py +++ /dev/null @@ -1,285 +0,0 @@ -import os -import re -import sys -import logging - -# Pipeline imports -import avalon.api -from avalon import io - -from openpype.lib import version_up -from openpype.hosts.fusion import api -from openpype.hosts.fusion.api import lib -from openpype.lib.avalon_context import get_workdir_from_session - -log = logging.getLogger("Update Slap Comp") - -self = sys.modules[__name__] -self._project = None - - -def _format_version_folder(folder): - """Format a version folder based on the filepath - - Assumption here is made that, if the path does not exists the folder - will be "v001" - - Args: - folder: file path to a folder - - Returns: - str: new version folder name - """ - - new_version = 1 - if os.path.isdir(folder): - re_version = re.compile(r"v\d+$") - versions = [i for i in os.listdir(folder) if os.path.isdir(i) - and re_version.match(i)] - if versions: - # ensure the "v" is not included - new_version = int(max(versions)[1:]) + 1 - - version_folder = "v{:03d}".format(new_version) - - return version_folder - - -def _get_fusion_instance(): - fusion = getattr(sys.modules["__main__"], "fusion", None) - if fusion is None: - try: - # Support for FuScript.exe, BlackmagicFusion module for py2 only - import BlackmagicFusion as bmf - fusion = bmf.scriptapp("Fusion") - except ImportError: - raise RuntimeError("Could not find a Fusion instance") - return fusion - - -def _format_filepath(session): - - project = session["AVALON_PROJECT"] - asset = session["AVALON_ASSET"] - - # Save updated slap comp - work_path = get_workdir_from_session(session) - walk_to_dir = os.path.join(work_path, "scenes", "slapcomp") - slapcomp_dir = os.path.abspath(walk_to_dir) - - # Ensure destination exists - if not os.path.isdir(slapcomp_dir): - log.warning("Folder did not exist, creating folder structure") - os.makedirs(slapcomp_dir) - - # Compute output path - new_filename = "{}_{}_slapcomp_v001.comp".format(project, asset) - new_filepath = os.path.join(slapcomp_dir, new_filename) - - # Create new unique filepath - if os.path.exists(new_filepath): - new_filepath = version_up(new_filepath) - - return new_filepath - - -def _update_savers(comp, session): - """Update all savers of the current comp to ensure the output is correct - - This will refactor the Saver file outputs to the renders of the new session - that is provided. - - In the case the original saver path had a path set relative to a /fusion/ - folder then that relative path will be matched with the exception of all - "version" (e.g. v010) references will be reset to v001. Otherwise only a - version folder will be computed in the new session's work "render" folder - to dump the files in and keeping the original filenames. - - Args: - comp (object): current comp instance - session (dict): the current Avalon session - - Returns: - None - """ - - new_work = get_workdir_from_session(session) - renders = os.path.join(new_work, "renders") - version_folder = _format_version_folder(renders) - renders_version = os.path.join(renders, version_folder) - - comp.Print("New renders to: %s\n" % renders) - - with api.comp_lock_and_undo_chunk(comp): - savers = comp.GetToolList(False, "Saver").values() - for saver in savers: - filepath = saver.GetAttrs("TOOLST_Clip_Name")[1.0] - - # Get old relative path to the "fusion" app folder so we can apply - # the same relative path afterwards. If not found fall back to - # using just a version folder with the filename in it. - # todo: can we make this less magical? - relpath = filepath.replace("\\", "/").rsplit("/fusion/", 1)[-1] - - if os.path.isabs(relpath): - # If not relative to a "/fusion/" folder then just use filename - filename = os.path.basename(filepath) - log.warning("Can't parse relative path, refactoring to only" - "filename in a version folder: %s" % filename) - new_path = os.path.join(renders_version, filename) - - else: - # Else reuse the relative path - # Reset version in folder and filename in the relative path - # to v001. The version should be is only detected when prefixed - # with either `_v` (underscore) or `/v` (folder) - version_pattern = r"(/|_)v[0-9]+" - if re.search(version_pattern, relpath): - new_relpath = re.sub(version_pattern, - r"\1v001", - relpath) - log.info("Resetting version folders to v001: " - "%s -> %s" % (relpath, new_relpath)) - relpath = new_relpath - - new_path = os.path.join(new_work, relpath) - - saver["Clip"] = new_path - - -def update_frame_range(comp, representations): - """Update the frame range of the comp and render length - - The start and end frame are based on the lowest start frame and the highest - end frame - - Args: - comp (object): current focused comp - representations (list) collection of dicts - - Returns: - None - - """ - - version_ids = [r["parent"] for r in representations] - versions = io.find({"type": "version", "_id": {"$in": version_ids}}) - versions = list(versions) - - versions = [v for v in versions - if v["data"].get("frameStart", None) is not None] - - if not versions: - log.warning("No versions loaded to match frame range to.\n") - return - - start = min(v["data"]["frameStart"] for v in versions) - end = max(v["data"]["frameEnd"] for v in versions) - - lib.update_frame_range(start, end, comp=comp) - - -def switch(asset_name, filepath=None, new=True): - """Switch the current containers of the file to the other asset (shot) - - Args: - filepath (str): file path of the comp file - asset_name (str): name of the asset (shot) - new (bool): Save updated comp under a different name - - Returns: - comp path (str): new filepath of the updated comp - - """ - - # If filepath provided, ensure it is valid absolute path - if filepath is not None: - if not os.path.isabs(filepath): - filepath = os.path.abspath(filepath) - - assert os.path.exists(filepath), "%s must exist " % filepath - - # Assert asset name exists - # It is better to do this here then to wait till switch_shot does it - asset = io.find_one({"type": "asset", "name": asset_name}) - assert asset, "Could not find '%s' in the database" % asset_name - - # Get current project - self._project = io.find_one({"type": "project", - "name": avalon.api.Session["AVALON_PROJECT"]}) - - # Go to comp - if not filepath: - current_comp = api.get_current_comp() - assert current_comp is not None, "Could not find current comp" - else: - fusion = _get_fusion_instance() - current_comp = fusion.LoadComp(filepath, quiet=True) - assert current_comp is not None, ( - "Fusion could not load '{}'").format(filepath) - - host = avalon.api.registered_host() - containers = list(host.ls()) - assert containers, "Nothing to update" - - representations = [] - for container in containers: - try: - representation = lib.switch_item( - container, - asset_name=asset_name) - representations.append(representation) - except Exception as e: - current_comp.Print("Error in switching! %s\n" % e.message) - - message = "Switched %i Loaders of the %i\n" % (len(representations), - len(containers)) - current_comp.Print(message) - - # Build the session to switch to - switch_to_session = avalon.api.Session.copy() - switch_to_session["AVALON_ASSET"] = asset['name'] - - if new: - comp_path = _format_filepath(switch_to_session) - - # Update savers output based on new session - _update_savers(current_comp, switch_to_session) - else: - comp_path = version_up(filepath) - - current_comp.Print(comp_path) - - current_comp.Print("\nUpdating frame range") - update_frame_range(current_comp, representations) - - current_comp.Save(comp_path) - - return comp_path - - -if __name__ == '__main__': - - # QUESTION: can we convert this to gui rather then standalone script? - # TODO: convert to gui tool - import argparse - - parser = argparse.ArgumentParser(description="Switch to a shot within an" - "existing comp file") - - parser.add_argument("--file_path", - type=str, - default=True, - help="File path of the comp to use") - - parser.add_argument("--asset_name", - type=str, - default=True, - help="Name of the asset (shot) to switch") - - args, unknown = parser.parse_args() - - avalon.api.install(api) - switch(args.asset_name, args.file_path) - - sys.exit(0) From 0941c186dffb75759f59fa2ca46bdde0fd2dc9c3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Mar 2022 19:44:42 +0200 Subject: [PATCH 12/58] Use new OpenPype Event System implemented with #2846 --- openpype/hosts/fusion/api/menu.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 7b23e2bc2b..b3d8e203c3 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -6,6 +6,7 @@ from avalon import api from openpype.tools.utils import host_tools from openpype.style import load_stylesheet +from openpype.lib import register_event_callback from openpype.hosts.fusion.scripts import ( set_rendermode, duplicate_with_inputs @@ -133,7 +134,7 @@ class OpenPypeMenu(QtWidgets.QWidget): fn() self._callbacks.append(_callback) - api.on(name, _callback) + register_event_callback(name, _callback) def deregister_all_callbacks(self): self._callbacks[:] = [] From 56892878c97b3244ef313cbd644241679773ecd3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Mar 2022 12:21:58 +0200 Subject: [PATCH 13/58] Cosmetics --- openpype/hosts/fusion/api/menu.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index b3d8e203c3..42dacfa0c0 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -185,7 +185,6 @@ class OpenPypeMenu(QtWidgets.QWidget): set_framerange() - def launch_openpype_menu(): app = QtWidgets.QApplication(sys.argv) app.setQuitOnLastWindowClosed(False) From 2d4061771d809c6d240e13965e845f04696ac0f9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Mar 2022 14:45:11 +0200 Subject: [PATCH 14/58] Shut down fusionscript process when OpenPype menu gets closed along with other Qt windows --- openpype/hosts/fusion/api/menu.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 42dacfa0c0..4a646c5e8f 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -187,7 +187,6 @@ class OpenPypeMenu(QtWidgets.QWidget): def launch_openpype_menu(): app = QtWidgets.QApplication(sys.argv) - app.setQuitOnLastWindowClosed(False) pype_menu = OpenPypeMenu() @@ -196,4 +195,6 @@ def launch_openpype_menu(): pype_menu.show() - sys.exit(app.exec_()) + result = app.exec_() + print("Shutting down..") + sys.exit(result) From 53382a1960e23dbc80235f9d0f25c79e8dff6d06 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Mar 2022 16:02:43 +0200 Subject: [PATCH 15/58] Draft implementation of Fusion heartbeat/pulse so UI closes after Fusion is closed --- openpype/hosts/fusion/api/menu.py | 6 +++ openpype/hosts/fusion/api/pulse.py | 62 ++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 openpype/hosts/fusion/api/pulse.py diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 4a646c5e8f..823670b9cf 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -15,6 +15,8 @@ from openpype.hosts.fusion.api import ( set_framerange ) +from .pulse import FusionPulse + class Spacer(QtWidgets.QWidget): def __init__(self, height, *args, **kwargs): @@ -121,6 +123,10 @@ class OpenPypeMenu(QtWidgets.QWidget): self.register_callback("taskChanged", self.on_task_changed) self.on_task_changed() + # Force close current process if Fusion is closed + self._pulse = FusionPulse(parent=self) + self._pulse.start() + def on_task_changed(self): # Update current context label label = api.Session["AVALON_ASSET"] diff --git a/openpype/hosts/fusion/api/pulse.py b/openpype/hosts/fusion/api/pulse.py new file mode 100644 index 0000000000..cad1c74e13 --- /dev/null +++ b/openpype/hosts/fusion/api/pulse.py @@ -0,0 +1,62 @@ +import os +import sys + +from Qt import QtCore, QtWidgets + + +class PulseThread(QtCore.QThread): + no_response = QtCore.Signal() + + def __init__(self, parent=None): + super(PulseThread, self).__init__(parent=parent) + + # Interval in milliseconds + self._interval = os.environ.get("OPENPYPE_FUSION_PULSE_INTERVAL", 1000) + + def run(self): + app = getattr(sys.modules["__main__"], "app", None) + + while True: + if self.isInterruptionRequested(): + return + try: + app.Test() + except Exception: + self.no_response.emit() + + self.msleep(self._interval) + + +class FusionPulse(QtCore.QObject): + """A Timer that checks whether host app is still alive. + + This checks whether the Fusion process is still active at a certain + interval. This is useful due to how Fusion runs its scripts. Each script + runs in its own environment and process (a `fusionscript` process each). + If Fusion would go down and we have a UI process running at the same time + then it can happen that the `fusionscript.exe` will remain running in the + background in limbo due to e.g. a Qt interface's QApplication that keeps + running infinitely. + + Warning: + When the host is not detected this will automatically exit + the current process. + + """ + + def __init__(self, parent=None): + super(FusionPulse, self).__init__(parent=parent) + self._thread = PulseThread(parent=self) + self._thread.no_response.connect(self.on_no_response) + + def on_no_response(self): + print("Pulse detected no response from Fusion..") + sys.exit(1) + + def start(self): + self._thread.start() + + def stop(self): + self._thread.requestInterruption() + + From 84ab53664f794314513fd6530890c4a1f5fca34b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Mar 2022 16:11:35 +0200 Subject: [PATCH 16/58] Take the `interval` variable into the run thread - not sure if really better --- openpype/hosts/fusion/api/pulse.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/api/pulse.py b/openpype/hosts/fusion/api/pulse.py index cad1c74e13..6d448a31c9 100644 --- a/openpype/hosts/fusion/api/pulse.py +++ b/openpype/hosts/fusion/api/pulse.py @@ -10,12 +10,12 @@ class PulseThread(QtCore.QThread): def __init__(self, parent=None): super(PulseThread, self).__init__(parent=parent) - # Interval in milliseconds - self._interval = os.environ.get("OPENPYPE_FUSION_PULSE_INTERVAL", 1000) - def run(self): app = getattr(sys.modules["__main__"], "app", None) + # Interval in milliseconds + interval = os.environ.get("OPENPYPE_FUSION_PULSE_INTERVAL", 1000) + while True: if self.isInterruptionRequested(): return From 0e9dd34b3d015a6d2ea96f32820aad378ecf09dd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Mar 2022 21:32:51 +0200 Subject: [PATCH 17/58] Fix code --- openpype/hosts/fusion/api/pulse.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/fusion/api/pulse.py b/openpype/hosts/fusion/api/pulse.py index 6d448a31c9..5b61f3bd63 100644 --- a/openpype/hosts/fusion/api/pulse.py +++ b/openpype/hosts/fusion/api/pulse.py @@ -1,7 +1,7 @@ import os import sys -from Qt import QtCore, QtWidgets +from Qt import QtCore class PulseThread(QtCore.QThread): @@ -24,7 +24,7 @@ class PulseThread(QtCore.QThread): except Exception: self.no_response.emit() - self.msleep(self._interval) + self.msleep(interval) class FusionPulse(QtCore.QObject): @@ -58,5 +58,3 @@ class FusionPulse(QtCore.QObject): def stop(self): self._thread.requestInterruption() - - From 229d31bc1ca10d51ab2b562ed128623a8895d26b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 13 Sep 2022 23:41:42 +0200 Subject: [PATCH 18/58] Collect global in/out as handles --- .../fusion/plugins/publish/collect_instances.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index b2192d1dd9..b36e43cacd 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -4,19 +4,21 @@ import pyblish.api def get_comp_render_range(comp): - """Return comp's start and end render range.""" + """Return comp's start-end render range and global start-end range.""" comp_attrs = comp.GetAttrs() start = comp_attrs["COMPN_RenderStart"] end = comp_attrs["COMPN_RenderEnd"] + global_start = comp_attrs["COMPN_GlobalStart"] + global_end = comp_attrs["COMPN_GlobalEnd"] # Whenever render ranges are undefined fall back # to the comp's global start and end if start == -1000000000: - start = comp_attrs["COMPN_GlobalEnd"] + start = global_start if end == -1000000000: - end = comp_attrs["COMPN_GlobalStart"] + end = global_end - return start, end + return start, end, global_start, global_end class CollectInstances(pyblish.api.ContextPlugin): @@ -42,9 +44,11 @@ class CollectInstances(pyblish.api.ContextPlugin): tools = comp.GetToolList(False).values() savers = [tool for tool in tools if tool.ID == "Saver"] - start, end = get_comp_render_range(comp) + 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: path = tool["Clip"][comp.TIME_UNDEFINED] @@ -78,6 +82,8 @@ class CollectInstances(pyblish.api.ContextPlugin): "label": label, "frameStart": context.data["frameStart"], "frameEnd": context.data["frameEnd"], + "frameStartHandle": context.data["frameStartHandle"], + "frameEndHandle": context.data["frameStartHandle"], "fps": context.data["fps"], "families": ["render", "review", "ftrack"], "family": "render", From 8d4d80c2258f2e6a3a7c799547b9940af74cfdb8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 13 Sep 2022 23:56:48 +0200 Subject: [PATCH 19/58] Be more explicit about the to render frame range (include rendering of handles) --- .../hosts/fusion/plugins/publish/render_local.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/render_local.py index 601c2ffccf..79e458b40a 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/render_local.py @@ -20,6 +20,8 @@ class Fusionlocal(pyblish.api.InstancePlugin): def process(self, instance): + # This plug-in runs only once and thus assumes all instances + # currently will render the same frame range context = instance.context key = "__hasRun{}".format(self.__class__.__name__) if context.data.get(key, False): @@ -28,8 +30,8 @@ class Fusionlocal(pyblish.api.InstancePlugin): context.data[key] = True current_comp = context.data["currentComp"] - frame_start = current_comp.GetAttrs("COMPN_RenderStart") - frame_end = current_comp.GetAttrs("COMPN_RenderEnd") + frame_start = context.data["frameStartHandle"] + frame_end = context.data["frameEndHandle"] path = instance.data["path"] output_dir = instance.data["outputDir"] @@ -40,7 +42,11 @@ class Fusionlocal(pyblish.api.InstancePlugin): self.log.info("End frame: {}".format(frame_end)) with comp_lock_and_undo_chunk(current_comp): - result = current_comp.Render() + result = current_comp.Render({ + "Start": frame_start, + "End": frame_end, + "Wait": True + }) if "representations" not in instance.data: instance.data["representations"] = [] From e12de9b3b2bfd2d28cc8cbeb620b01babca54e6d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 14 Sep 2022 00:02:14 +0200 Subject: [PATCH 20/58] Do not auto-add ftrack family - That should be left up to plug-ins in Ftrack module --- openpype/hosts/fusion/plugins/publish/collect_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index b36e43cacd..fe60b83827 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -85,7 +85,7 @@ class CollectInstances(pyblish.api.ContextPlugin): "frameStartHandle": context.data["frameStartHandle"], "frameEndHandle": context.data["frameStartHandle"], "fps": context.data["fps"], - "families": ["render", "review", "ftrack"], + "families": ["render", "review"], "family": "render", "active": active, "publish": active # backwards compatibility From df370e5d3cd321b3a34a37b7247ccf356ae9e053 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Sep 2022 23:43:09 +0200 Subject: [PATCH 21/58] Refactor to match with API changes of OpenPype --- openpype/hosts/fusion/api/lib.py | 8 ++++---- openpype/hosts/fusion/api/menu.py | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index db8dfb2795..19242da304 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -17,10 +17,10 @@ from openpype.pipeline import ( switch_container, legacy_io, ) +from openpype.pipeline.context_tools import get_current_project_asset + from .pipeline import get_current_comp, comp_lock_and_undo_chunk -from openpype.api import ( - get_asset -) + self = sys.modules[__name__] self._project = None @@ -70,7 +70,7 @@ def update_frame_range(start, end, comp=None, set_render_range=True, **kwargs): def set_framerange(): - asset_doc = get_asset() + asset_doc = get_current_project_asset() start = asset_doc["data"]["frameStart"] end = asset_doc["data"]["frameEnd"] diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 823670b9cf..c5eb093247 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -2,9 +2,7 @@ import sys from Qt import QtWidgets, QtCore -from avalon import api from openpype.tools.utils import host_tools - from openpype.style import load_stylesheet from openpype.lib import register_event_callback from openpype.hosts.fusion.scripts import ( @@ -14,6 +12,7 @@ from openpype.hosts.fusion.scripts import ( from openpype.hosts.fusion.api import ( set_framerange ) +from openpype.pipeline import legacy_io from .pulse import FusionPulse @@ -129,7 +128,7 @@ class OpenPypeMenu(QtWidgets.QWidget): def on_task_changed(self): # Update current context label - label = api.Session["AVALON_ASSET"] + label = legacy_io.Session["AVALON_ASSET"] self.asset_label.setText(label) def register_callback(self, name, fn): From 618e6267b48b44dcc62615f33489e199a00a19bd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Sep 2022 23:43:42 +0200 Subject: [PATCH 22/58] Allow to minimize the menu so it doesn't always have to stay on top --- openpype/hosts/fusion/api/menu.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index c5eb093247..bba94053a2 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -44,6 +44,7 @@ class OpenPypeMenu(QtWidgets.QWidget): QtCore.Qt.Window | QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint + | QtCore.Qt.WindowMinimizeButtonHint | QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowStaysOnTopHint ) From 83fb00e0ffbbceb969aff2cf0865af08545784ca Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Sep 2022 23:54:28 +0200 Subject: [PATCH 23/58] Get start frame including handles --- .../fusion/plugins/load/load_sequence.py | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/fusion/plugins/load/load_sequence.py b/openpype/hosts/fusion/plugins/load/load_sequence.py index abd0f4e411..faac942c53 100644 --- a/openpype/hosts/fusion/plugins/load/load_sequence.py +++ b/openpype/hosts/fusion/plugins/load/load_sequence.py @@ -149,9 +149,8 @@ class FusionLoadSequence(load.LoaderPlugin): tool["Clip"] = path # Set global in point to start frame (if in version.data) - start = context["version"]["data"].get("frameStart", None) - if start is not None: - loader_shift(tool, start, relative=False) + start = self._get_start(context["version"], tool) + loader_shift(tool, start, relative=False) imprint_container(tool, name=name, @@ -214,12 +213,7 @@ class FusionLoadSequence(load.LoaderPlugin): # Get start frame from version data project_name = legacy_io.active_project() version = get_version_by_id(project_name, representation["parent"]) - start = version["data"].get("frameStart") - if start is None: - self.log.warning("Missing start frame for updated version" - "assuming starts at frame 0 for: " - "{} ({})".format(tool.Name, representation)) - start = 0 + start = self._get_start(version, tool) with comp_lock_and_undo_chunk(comp, "Update Loader"): @@ -256,3 +250,27 @@ class FusionLoadSequence(load.LoaderPlugin): """Get first file in representation root""" files = sorted(os.listdir(root)) return os.path.join(root, files[0]) + + def _get_start(self, version_doc, tool): + """Return real start frame of published files (incl. handles)""" + data = version_doc["data"] + + # Get start frame directly with handle if it's in data + start = data.get("frameStartHandle") + if start is not None: + return start + + # Get frame start without handles + start = data.get("frameStart") + if start is None: + self.log.warning("Missing start frame for version " + "assuming starts at frame 0 for: " + "{}".format(tool.Name)) + return 0 + + # Use `handleStart` if the data is available + handle_start = data.get("handleStart") + if handle_start: + start -= handle_start + + return start From 0806534a278a6ae1a95f1f4f62ba17a86f89d5e0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Sep 2022 23:56:09 +0200 Subject: [PATCH 24/58] Return early if no need to shift --- openpype/hosts/fusion/plugins/load/load_sequence.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/fusion/plugins/load/load_sequence.py b/openpype/hosts/fusion/plugins/load/load_sequence.py index faac942c53..1614704090 100644 --- a/openpype/hosts/fusion/plugins/load/load_sequence.py +++ b/openpype/hosts/fusion/plugins/load/load_sequence.py @@ -101,6 +101,9 @@ def loader_shift(loader, frame, relative=True): else: shift = frame - old_in + if not shift: + return + # Shifting global in will try to automatically compensate for the change # in the "ClipTimeStart" and "HoldFirstFrame" inputs, so we preserve those # input values to "just shift" the clip From 5058de28881aee71f8681034b4caa18f6b7b0605 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Sep 2022 23:56:46 +0200 Subject: [PATCH 25/58] Fix return value --- openpype/hosts/fusion/plugins/load/load_sequence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/load/load_sequence.py b/openpype/hosts/fusion/plugins/load/load_sequence.py index 1614704090..6f44c61d1b 100644 --- a/openpype/hosts/fusion/plugins/load/load_sequence.py +++ b/openpype/hosts/fusion/plugins/load/load_sequence.py @@ -102,7 +102,7 @@ def loader_shift(loader, frame, relative=True): shift = frame - old_in if not shift: - return + return 0 # Shifting global in will try to automatically compensate for the change # in the "ClipTimeStart" and "HoldFirstFrame" inputs, so we preserve those From df4d92d973c7d85561745dae167d744da6ac5e56 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Sep 2022 22:48:27 +0200 Subject: [PATCH 26/58] Hack in support for Fusion 18 (cherry picked from commit 5a75db9d7ba1f4df78e84f1315e8e8d9c9a357c0) --- openpype/hosts/fusion/hooks/pre_fusion_setup.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/fusion/hooks/pre_fusion_setup.py b/openpype/hosts/fusion/hooks/pre_fusion_setup.py index c78d433e5c..ec5889a88a 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_setup.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_setup.py @@ -33,6 +33,13 @@ class FusionPrelaunch(PreLaunchHook): self.log.info(f"Setting {py36_var}: '{py36_dir}'...") self.launch_context.env[py36_var] = py36_dir + # TODO: Set this for EITHER Fu16-17 OR Fu18+, don't do both + # Fusion 18+ does not look in FUSION16_PYTHON36_HOME anymore + # but instead uses FUSION_PYTHON3_HOME and requires the Python to + # be available on PATH to work. So let's enforce that for now. + self.launch_context.env["FUSION_PYTHON3_HOME"] = py36_dir + self.launch_context.env["PATH"] += ";" + py36_dir + # Add our Fusion Master Prefs which is the only way to customize # Fusion to define where it can read custom scripts and tools from self.log.info(f"Setting OPENPYPE_FUSION: {HOST_DIR}") From 0e4a73f6808c6c22a7ffa45a97589c7990473829 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Sep 2022 23:22:56 +0200 Subject: [PATCH 27/58] Fusion: Set OCIO project setting and set OCIO env var on launch (cherry picked from commit 8e9a7200d35ab7cba174855acfe03794936125cf) --- .../fusion/hooks/pre_fusion_ocio_hook.py | 40 +++++++++++++++++++ .../defaults/project_anatomy/imageio.json | 10 +++++ .../schemas/schema_anatomy_imageio.json | 29 ++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py diff --git a/openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py b/openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py new file mode 100644 index 0000000000..f7c7bc0b4c --- /dev/null +++ b/openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py @@ -0,0 +1,40 @@ +import os +import platform + +from openpype.lib import PreLaunchHook, ApplicationLaunchFailed + + +class FusionPreLaunchOCIO(PreLaunchHook): + """Set OCIO environment variable for Fusion""" + app_groups = ["fusion"] + + def execute(self): + """Hook entry method.""" + + # get image io + project_anatomy = self.data["anatomy"] + + # make sure anatomy settings are having flame key + imageio_fusion = project_anatomy["imageio"].get("fusion") + if not imageio_fusion: + raise ApplicationLaunchFailed(( + "Anatomy project settings are missing `fusion` key. " + "Please make sure you remove project overrides on " + "Anatomy ImageIO") + ) + + ocio = imageio_fusion.get("ocio") + enabled = ocio.get("enabled", False) + if not enabled: + return + + platform_key = platform.system().lower() + ocio_path = ocio["configFilePath"][platform_key] + if not ocio_path: + raise ApplicationLaunchFailed( + "Fusion OCIO is enabled in project settings but no OCIO config" + f"path is set for your current platform: {platform_key}" + ) + + self.log.info(f"Setting OCIO config path: {ocio_path}") + self.launch_context.env["OCIO"] = os.pathsep.join(ocio_path) diff --git a/openpype/settings/defaults/project_anatomy/imageio.json b/openpype/settings/defaults/project_anatomy/imageio.json index f0be8f95f4..9b5e8639b1 100644 --- a/openpype/settings/defaults/project_anatomy/imageio.json +++ b/openpype/settings/defaults/project_anatomy/imageio.json @@ -236,6 +236,16 @@ "viewTransform": "sRGB gamma" } }, + "fusion": { + "ocio": { + "enabled": false, + "configFilePath": { + "windows": [], + "darwin": [], + "linux": [] + } + } + }, "flame": { "project": { "colourPolicy": "ACES 1.1", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json index ef8c907dda..644463fece 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json @@ -427,6 +427,35 @@ } ] }, + + { + "key": "fusion", + "type": "dict", + "label": "Fusion", + "children": [ + { + "key": "ocio", + "type": "dict", + "label": "OCIO", + "collapsible": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Set OCIO variable for Fusion" + }, + { + "type": "path", + "key": "configFilePath", + "label": "OCIO Config File Path", + "multiplatform": true, + "multipath": true + } + ] + } + ] + }, { "key": "flame", "type": "dict", From cd40fab99ff2627bf29aa2c9de7a884d7640ff7e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 16 Sep 2022 00:01:41 +0200 Subject: [PATCH 28/58] Add Fusion 18 application to defaults --- .../defaults/system_settings/applications.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 30b0a5cbe3..4a3e0c1b94 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -730,6 +730,21 @@ "OPENPYPE_LOG_NO_COLORS": "Yes" }, "variants": { + "18": { + "executables": { + "windows": [ + "C:\\Program Files\\Blackmagic Design\\Fusion 18\\Fusion.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": {} + }, "17": { "executables": { "windows": [ From 839ded23bad51e9e949e960794baceaf4f4d9958 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 16 Sep 2022 00:35:37 +0200 Subject: [PATCH 29/58] Make `handle_start` and `handle_end` more explicit arguments --- openpype/hosts/fusion/api/lib.py | 34 ++++++++++++++------------------ 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 19242da304..dcf205ff6a 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -25,7 +25,8 @@ self = sys.modules[__name__] self._project = None -def update_frame_range(start, end, comp=None, set_render_range=True, **kwargs): +def update_frame_range(start, end, comp=None, set_render_range=True, + handle_start=0, handle_end=0): """Set Fusion comp's start and end frame range Args: @@ -34,7 +35,8 @@ def update_frame_range(start, end, comp=None, set_render_range=True, **kwargs): comp (object, Optional): comp object from fusion set_render_range (bool, Optional): When True this will also set the composition's render start and end frame. - kwargs (dict): additional kwargs + handle_start (float, int, Optional): frame handles before start frame + handle_end (float, int, Optional): frame handles after end frame Returns: None @@ -44,20 +46,15 @@ def update_frame_range(start, end, comp=None, set_render_range=True, **kwargs): if not comp: comp = get_current_comp() + # Convert any potential none type to zero + handle_start = handle_start or 0 + handle_end = handle_end or 0 + attrs = { - "COMPN_GlobalStart": start, - "COMPN_GlobalEnd": end + "COMPN_GlobalStart": start - handle_start, + "COMPN_GlobalEnd": end + handle_end } - # exclude handles if any found in kwargs - if kwargs.get("handle_start"): - handle_start = kwargs.get("handle_start") - attrs["COMPN_GlobalStart"] = int(start - handle_start) - - if kwargs.get("handle_end"): - handle_end = kwargs.get("handle_end") - attrs["COMPN_GlobalEnd"] = int(end + handle_end) - # set frame range if set_render_range: attrs.update({ @@ -73,12 +70,11 @@ def set_framerange(): asset_doc = get_current_project_asset() start = asset_doc["data"]["frameStart"] end = asset_doc["data"]["frameEnd"] - - data = { - "handle_start": asset_doc["data"]["handleStart"], - "handle_end": asset_doc["data"]["handleEnd"] - } - update_frame_range(start, end, set_render_range=True, **data) + handle_start = asset_doc["data"]["handleStart"], + handle_end = asset_doc["data"]["handleEnd"], + update_frame_range(start, end, set_render_range=True, + handle_start=handle_start, + handle_end=handle_end) def get_additional_data(container): From 508b17963d09bda307051d2483fd83753ca080b1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 16 Sep 2022 15:41:16 +0200 Subject: [PATCH 30/58] Move Fusion OCIO settings out of anatomy into project settings --- .../defaults/project_anatomy/imageio.json | 10 ----- .../defaults/project_settings/fusion.json | 12 ++++++ .../schemas/projects_schema/schema_main.json | 4 ++ .../schema_project_fusion.json | 38 +++++++++++++++++++ .../schemas/schema_anatomy_imageio.json | 29 -------------- 5 files changed, 54 insertions(+), 39 deletions(-) create mode 100644 openpype/settings/defaults/project_settings/fusion.json create mode 100644 openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json diff --git a/openpype/settings/defaults/project_anatomy/imageio.json b/openpype/settings/defaults/project_anatomy/imageio.json index 9b5e8639b1..f0be8f95f4 100644 --- a/openpype/settings/defaults/project_anatomy/imageio.json +++ b/openpype/settings/defaults/project_anatomy/imageio.json @@ -236,16 +236,6 @@ "viewTransform": "sRGB gamma" } }, - "fusion": { - "ocio": { - "enabled": false, - "configFilePath": { - "windows": [], - "darwin": [], - "linux": [] - } - } - }, "flame": { "project": { "colourPolicy": "ACES 1.1", diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json new file mode 100644 index 0000000000..1b4c4c55b5 --- /dev/null +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -0,0 +1,12 @@ +{ + "imageio": { + "ocio": { + "enabled": false, + "configFilePath": { + "windows": [], + "darwin": [], + "linux": [] + } + } + } +} \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_main.json b/openpype/settings/entities/schemas/projects_schema/schema_main.json index 80b1baad1b..0b9fbf7470 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_main.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_main.json @@ -90,6 +90,10 @@ "type": "schema", "name": "schema_project_nuke" }, + { + "type": "schema", + "name": "schema_project_fusion" + }, { "type": "schema", "name": "schema_project_hiero" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json new file mode 100644 index 0000000000..8f98a8173f --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -0,0 +1,38 @@ +{ + "type": "dict", + "collapsible": true, + "key": "fusion", + "label": "Fusion", + "is_file": true, + "children": [ + { + "key": "imageio", + "type": "dict", + "label": "Color Management (ImageIO)", + "collapsible": true, + "children": [ + { + "key": "ocio", + "type": "dict", + "label": "OpenColorIO (OCIO)", + "collapsible": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Set OCIO variable for Fusion" + }, + { + "type": "path", + "key": "configFilePath", + "label": "OCIO Config File Path", + "multiplatform": true, + "multipath": true + } + ] + } + ] + } + ] +} diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json index 644463fece..ef8c907dda 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json @@ -427,35 +427,6 @@ } ] }, - - { - "key": "fusion", - "type": "dict", - "label": "Fusion", - "children": [ - { - "key": "ocio", - "type": "dict", - "label": "OCIO", - "collapsible": true, - "checkbox_key": "enabled", - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Set OCIO variable for Fusion" - }, - { - "type": "path", - "key": "configFilePath", - "label": "OCIO Config File Path", - "multiplatform": true, - "multipath": true - } - ] - } - ] - }, { "key": "flame", "type": "dict", From 7aa905898e6726e910c05be8db6514e5e656b8c1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 16 Sep 2022 15:43:11 +0200 Subject: [PATCH 31/58] Use new settings location --- openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py b/openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py index f7c7bc0b4c..12fc640f5c 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py @@ -12,10 +12,10 @@ class FusionPreLaunchOCIO(PreLaunchHook): """Hook entry method.""" # get image io - project_anatomy = self.data["anatomy"] + project_settings = self.data["project_settings"] # make sure anatomy settings are having flame key - imageio_fusion = project_anatomy["imageio"].get("fusion") + imageio_fusion = project_settings.get("fusion", {}).get("imageio") if not imageio_fusion: raise ApplicationLaunchFailed(( "Anatomy project settings are missing `fusion` key. " From bd3f6acb0275ee90b3107f8ccdf4b9e8a6e2770b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 Sep 2022 14:35:44 +0200 Subject: [PATCH 32/58] Refactor `HOST_DIR` to `FUSION_HOST_DIR` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Ježek --- openpype/hosts/fusion/hooks/pre_fusion_setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/hooks/pre_fusion_setup.py b/openpype/hosts/fusion/hooks/pre_fusion_setup.py index ec5889a88a..a0796de10a 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_setup.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_setup.py @@ -1,6 +1,6 @@ import os from openpype.lib import PreLaunchHook, ApplicationLaunchFailed -from openpype.hosts.fusion import HOST_DIR +from openpype.hosts.fusion import FUSION_HOST_DIR class FusionPrelaunch(PreLaunchHook): @@ -42,8 +42,8 @@ class FusionPrelaunch(PreLaunchHook): # Add our Fusion Master Prefs which is the only way to customize # Fusion to define where it can read custom scripts and tools from - self.log.info(f"Setting OPENPYPE_FUSION: {HOST_DIR}") - self.launch_context.env["OPENPYPE_FUSION"] = HOST_DIR + self.log.info(f"Setting OPENPYPE_FUSION: {FUSION_HOST_DIR}") + self.launch_context.env["OPENPYPE_FUSION"] = FUSION_HOST_DIR pref_var = "FUSION16_MasterPrefs" # used by both Fu16 and Fu17 prefs = os.path.join(HOST_DIR, "deploy", "fusion_shared.prefs") From daf29122e6f7be1b6cc4d3171423dc61764d726d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 Sep 2022 14:36:36 +0200 Subject: [PATCH 33/58] Refactor `HOST_DIR` to `FUSION_HOST_DIR` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Ježek --- openpype/hosts/fusion/hooks/pre_fusion_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/hooks/pre_fusion_setup.py b/openpype/hosts/fusion/hooks/pre_fusion_setup.py index a0796de10a..0ba7e92bb1 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_setup.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_setup.py @@ -46,6 +46,6 @@ class FusionPrelaunch(PreLaunchHook): self.launch_context.env["OPENPYPE_FUSION"] = FUSION_HOST_DIR pref_var = "FUSION16_MasterPrefs" # used by both Fu16 and Fu17 - prefs = os.path.join(HOST_DIR, "deploy", "fusion_shared.prefs") + prefs = os.path.join(FUSION_HOST_DIR, "deploy", "fusion_shared.prefs") self.log.info(f"Setting {pref_var}: {prefs}") self.launch_context.env[pref_var] = prefs From 5694cff114a3f1f3a42461d72bb03cff9521bb58 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 Sep 2022 14:47:45 +0200 Subject: [PATCH 34/58] Use `FUSION_PYTHON3_HOME` instead of `FUSION16_PYTHON36_HOME` as input variable --- .../hosts/fusion/hooks/pre_fusion_setup.py | 56 +++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/fusion/hooks/pre_fusion_setup.py b/openpype/hosts/fusion/hooks/pre_fusion_setup.py index 0ba7e92bb1..d043d54322 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_setup.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_setup.py @@ -4,48 +4,58 @@ from openpype.hosts.fusion import FUSION_HOST_DIR class FusionPrelaunch(PreLaunchHook): - """ - This hook will check if current workfile path has Fusion - project inside. + """Prepares OpenPype Fusion environment + + Requires FUSION_PYTHON3_HOME to be defined in the environment for Fusion + to point at a valid Python 3 build for Fusion. That is Python 3.3-3.10 + for Fusion 18 and Fusion 3.6 for Fusion 16 and 17. + + This also sets FUSION16_MasterPrefs to apply the fusion master prefs + as set in openpype/hosts/fusion/deploy/fusion_shared.prefs to enable + the OpenPype menu and force Python 3 over Python 2. + """ app_groups = ["fusion"] def execute(self): - # making sure python 3.6 is installed at provided path - py36_var = "FUSION16_PYTHON36_HOME" - fusion_python36_home = self.launch_context.env.get(py36_var, "") + # making sure python 3 is installed at provided path + # Py 3.3-3.10 for Fusion 18+ or Py 3.6 for Fu 16-17 + py3_var = "FUSION_PYTHON3_HOME" + fusion_python3_home = self.launch_context.env.get(py3_var, "") - self.log.info(f"Looking for Python 3.6 in: {fusion_python36_home}") - for path in fusion_python36_home.split(os.pathsep): + self.log.info(f"Looking for Python 3 in: {fusion_python3_home}") + for path in fusion_python3_home.split(os.pathsep): # Allow defining multiple paths to allow "fallback" to other # path. But make to set only a single path as final variable. - py36_dir = os.path.normpath(path) - if os.path.isdir(py36_dir): + py3_dir = os.path.normpath(path) + if os.path.isdir(py3_dir): break else: raise ApplicationLaunchFailed( - "Python 3.6 is not installed at the provided path.\n" - "Either make sure the environments in fusion settings has" - " 'PYTHON36' set corectly or make sure Python 3.6 is installed" - f" in the given path.\n\nPYTHON36: {fusion_python36_home}" + "Python 3 is not installed at the provided path.\n" + "Make sure the environment in fusion settings has " + "'FUSION_PYTHON3_HOME' set correctly and make sure " + "Python 3 is installed in the given path." + f"\n\nPYTHON36: {fusion_python3_home}" ) - self.log.info(f"Setting {py36_var}: '{py36_dir}'...") - self.launch_context.env[py36_var] = py36_dir + self.log.info(f"Setting {py3_var}: '{py3_dir}'...") + self.launch_context.env[py3_var] = py3_dir - # TODO: Set this for EITHER Fu16-17 OR Fu18+, don't do both - # Fusion 18+ does not look in FUSION16_PYTHON36_HOME anymore - # but instead uses FUSION_PYTHON3_HOME and requires the Python to - # be available on PATH to work. So let's enforce that for now. - self.launch_context.env["FUSION_PYTHON3_HOME"] = py36_dir - self.launch_context.env["PATH"] += ";" + py36_dir + # Fusion 18+ requires FUSION_PYTHON3_HOME to also be on PATH + self.launch_context.env["PATH"] += ";" + py3_dir + + # Fusion 16 and 17 use FUSION16_PYTHON36_HOME instead of + # FUSION_PYTHON3_HOME and will only work with a Python 3.6 version + # TODO: Detect Fusion version to only set for specific Fusion build + self.launch_context.env["FUSION16_PYTHON36_HOME"] = py3_dir # Add our Fusion Master Prefs which is the only way to customize # Fusion to define where it can read custom scripts and tools from self.log.info(f"Setting OPENPYPE_FUSION: {FUSION_HOST_DIR}") self.launch_context.env["OPENPYPE_FUSION"] = FUSION_HOST_DIR - pref_var = "FUSION16_MasterPrefs" # used by both Fu16 and Fu17 + pref_var = "FUSION16_MasterPrefs" # used by Fusion 16, 17 and 18 prefs = os.path.join(FUSION_HOST_DIR, "deploy", "fusion_shared.prefs") self.log.info(f"Setting {pref_var}: {prefs}") self.launch_context.env[pref_var] = prefs From c965b35549ebcde4b88380f1320376f52781a946 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 Sep 2022 15:55:10 +0200 Subject: [PATCH 35/58] Fix typo --- openpype/hosts/fusion/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index dcf205ff6a..314acb7e78 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -70,8 +70,8 @@ def set_framerange(): asset_doc = get_current_project_asset() start = asset_doc["data"]["frameStart"] end = asset_doc["data"]["frameEnd"] - handle_start = asset_doc["data"]["handleStart"], - handle_end = asset_doc["data"]["handleEnd"], + handle_start = asset_doc["data"]["handleStart"] + handle_end = asset_doc["data"]["handleEnd"] update_frame_range(start, end, set_render_range=True, handle_start=handle_start, handle_end=handle_end) From 4154f76830f2bb96abccf8090ebf9b43c943ff6c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 Sep 2022 18:03:06 +0200 Subject: [PATCH 36/58] Implement Set Asset Resolution --- openpype/hosts/fusion/api/lib.py | 15 +++++++++++++++ openpype/hosts/fusion/api/menu.py | 6 ++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 314acb7e78..b7e72e4c7a 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -77,6 +77,21 @@ def set_framerange(): handle_end=handle_end) +def set_resolution(): + """Set Comp's defaults""" + asset_doc = get_current_project_asset() + width = asset_doc["data"]["resolutionWidth"] + height = asset_doc["data"]["resolutionHeight"] + comp = get_current_comp() + + print("Setting comp frame format resolution to {}x{}".format(width, + height)) + comp.SetPrefs({ + "Comp.FrameFormat.Width": width, + "Comp.FrameFormat.Height": height, + }) + + def get_additional_data(container): """Get Fusion related data for the container diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index bba94053a2..84f680a918 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -9,8 +9,9 @@ from openpype.hosts.fusion.scripts import ( set_rendermode, duplicate_with_inputs ) -from openpype.hosts.fusion.api import ( - set_framerange +from openpype.hosts.fusion.api.lib import ( + set_framerange, + set_resolution ) from openpype.pipeline import legacy_io @@ -185,6 +186,7 @@ class OpenPypeMenu(QtWidgets.QWidget): def on_set_resolution_clicked(self): print("Clicked Reset Resolution") + set_resolution() def on_set_framerange_clicked(self): print("Clicked Reset Framerange") From 830ccf0438947f6c2891611293e0f26b76874411 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 Sep 2022 18:04:58 +0200 Subject: [PATCH 37/58] Clarify function names --- openpype/hosts/fusion/api/__init__.py | 4 ++-- openpype/hosts/fusion/api/lib.py | 4 ++-- openpype/hosts/fusion/api/menu.py | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/fusion/api/__init__.py b/openpype/hosts/fusion/api/__init__.py index 78afabdb45..e5a7dac8c8 100644 --- a/openpype/hosts/fusion/api/__init__.py +++ b/openpype/hosts/fusion/api/__init__.py @@ -24,7 +24,7 @@ from .lib import ( maintained_selection, get_additional_data, update_frame_range, - set_framerange + set_asset_framerange ) from .menu import launch_openpype_menu @@ -54,7 +54,7 @@ __all__ = [ "maintained_selection", "get_additional_data", "update_frame_range", - "set_framerange", + "set_asset_framerange", # menu "launch_openpype_menu", diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index b7e72e4c7a..85afc11e1c 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -66,7 +66,7 @@ def update_frame_range(start, end, comp=None, set_render_range=True, comp.SetAttrs(attrs) -def set_framerange(): +def set_asset_framerange(): asset_doc = get_current_project_asset() start = asset_doc["data"]["frameStart"] end = asset_doc["data"]["frameEnd"] @@ -77,7 +77,7 @@ def set_framerange(): handle_end=handle_end) -def set_resolution(): +def set_asset_resolution(): """Set Comp's defaults""" asset_doc = get_current_project_asset() width = asset_doc["data"]["resolutionWidth"] diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 84f680a918..4819fd6c7c 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -10,8 +10,8 @@ from openpype.hosts.fusion.scripts import ( duplicate_with_inputs ) from openpype.hosts.fusion.api.lib import ( - set_framerange, - set_resolution + set_asset_framerange, + set_asset_resolution ) from openpype.pipeline import legacy_io @@ -186,11 +186,11 @@ class OpenPypeMenu(QtWidgets.QWidget): def on_set_resolution_clicked(self): print("Clicked Reset Resolution") - set_resolution() + set_asset_resolution() def on_set_framerange_clicked(self): print("Clicked Reset Framerange") - set_framerange() + set_asset_framerange() def launch_openpype_menu(): From 863265fb41cbb7aec7111833658fd21c4f38af23 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 Sep 2022 18:06:22 +0200 Subject: [PATCH 38/58] Set `OPENPYPE_LOG_NO_COLORS` in addon --- openpype/hosts/fusion/addon.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/hosts/fusion/addon.py b/openpype/hosts/fusion/addon.py index e257005061..1913cc2e30 100644 --- a/openpype/hosts/fusion/addon.py +++ b/openpype/hosts/fusion/addon.py @@ -19,5 +19,14 @@ class FusionAddon(OpenPypeModule, IHostAddon): os.path.join(FUSION_HOST_DIR, "hooks") ] + def add_implementation_envs(self, env, _app): + # Set default values if are not already set via settings + defaults = { + "OPENPYPE_LOG_NO_COLORS": "Yes" + } + for key, value in defaults.items(): + if not env.get(key): + env[key] = value + def get_workfile_extensions(self): return [".comp"] From df330e1002420a51b87117526f5c7a910f152279 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 Sep 2022 18:07:24 +0200 Subject: [PATCH 39/58] Update defaults for Fusion environment --- .../system_settings/applications.json | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 4a3e0c1b94..c37c3d299e 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -706,28 +706,11 @@ "icon": "{}/app_icons/fusion.png", "host_name": "fusion", "environment": { - "FUSION_UTILITY_SCRIPTS_SOURCE_DIR": [], - "FUSION_UTILITY_SCRIPTS_DIR": { - "windows": "{PROGRAMDATA}/Blackmagic Design/Fusion/Scripts/Comp", - "darwin": "/Library/Application Support/Blackmagic Design/Fusion/Scripts/Comp", - "linux": "/opt/Fusion/Scripts/Comp" - }, - "PYTHON36": { + "FUSION_PYTHON3_HOME": { "windows": "{LOCALAPPDATA}/Programs/Python/Python36", "darwin": "~/Library/Python/3.6/bin", "linux": "/opt/Python/3.6/bin" - }, - "PYTHONPATH": [ - "{PYTHON36}/Lib/site-packages", - "{VIRTUAL_ENV}/Lib/site-packages", - "{PYTHONPATH}" - ], - "PATH": [ - "{PYTHON36}", - "{PYTHON36}/Scripts", - "{PATH}" - ], - "OPENPYPE_LOG_NO_COLORS": "Yes" + } }, "variants": { "18": { From c725c6affbcf474a7b9f9430e3d26933898ebb1d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 Sep 2022 18:12:41 +0200 Subject: [PATCH 40/58] Tweak docstrings --- openpype/hosts/fusion/api/lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 85afc11e1c..91a74ac848 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -67,6 +67,7 @@ def update_frame_range(start, end, comp=None, set_render_range=True, def set_asset_framerange(): + """Set Comp's frame range based on current asset""" asset_doc = get_current_project_asset() start = asset_doc["data"]["frameStart"] end = asset_doc["data"]["frameEnd"] @@ -78,7 +79,7 @@ def set_asset_framerange(): def set_asset_resolution(): - """Set Comp's defaults""" + """Set Comp's resolution width x height default based on current asset""" asset_doc = get_current_project_asset() width = asset_doc["data"]["resolutionWidth"] height = asset_doc["data"]["resolutionHeight"] From 1f035a1eee956d56b55e86dd97f5090fdab81894 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 01:24:24 +0200 Subject: [PATCH 41/58] Store link to menu so we can get access to it elsewhere --- openpype/hosts/fusion/api/menu.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 4819fd6c7c..cf3dea8ec3 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -17,6 +17,9 @@ from openpype.pipeline import legacy_io from .pulse import FusionPulse +self = sys.modules[__name__] +self.menu = None + class Spacer(QtWidgets.QWidget): def __init__(self, height, *args, **kwargs): @@ -202,6 +205,7 @@ def launch_openpype_menu(): pype_menu.setStyleSheet(stylesheet) pype_menu.show() + self.menu = pype_menu result = app.exec_() print("Shutting down..") From 23b6a35266ef14f8bccbe8edfc1551ce63a0f0b6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 01:26:20 +0200 Subject: [PATCH 42/58] Add after open callback to show popup about outdated containers --- openpype/hosts/fusion/api/pipeline.py | 39 ++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 7f9a57dc0f..b5d461e7f0 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -8,7 +8,10 @@ import contextlib import pyblish.api -from openpype.lib import Logger +from openpype.lib import ( + Logger, + register_event_callback +) from openpype.pipeline import ( register_loader_plugin_path, register_creator_plugin_path, @@ -18,6 +21,7 @@ from openpype.pipeline import ( deregister_inventory_action_path, AVALON_CONTAINER_ID, ) +from openpype.pipeline.load import any_outdated_containers from openpype.hosts.fusion import FUSION_HOST_DIR log = Logger.get_logger(__name__) @@ -77,6 +81,11 @@ def install(): "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 @@ -125,6 +134,34 @@ def on_pyblish_instance_toggled(instance, old_value, new_value): tool.SetAttrs({"TOOLB_PassThrough": passthrough}) +def on_after_open(_event): + + if any_outdated_containers(): + log.warning("Scene has outdated content.") + + # Find OpenPype menu to attach to + from . import menu + + comp = get_current_comp() + + def _on_show_scene_inventory(): + comp.CurrentFrame.ActivateFrame() # ensure that comp is active + host_tools.show_scene_inventory() + + from openpype.widgets import popup + from openpype.style import load_stylesheet + dialog = popup.Popup(parent=menu.menu) + dialog.setWindowTitle("Fusion comp has outdated content") + dialog.setMessage("There are outdated containers in " + "your Fusion comp.") + dialog.on_clicked.connect(_on_show_scene_inventory) + + dialog.show() + dialog.raise_() + dialog.activateWindow() + dialog.setStyleSheet(load_stylesheet()) + + def ls(): """List containers from active Fusion scene From 30ee358fc1e7b127981fcf4c1e897e7dc65a0de7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 01:29:01 +0200 Subject: [PATCH 43/58] Move `get_current_comp` and `comp_lock_and_undo_chunk` to `lib` + fix import of host tools --- openpype/hosts/fusion/api/__init__.py | 14 ++++++-------- openpype/hosts/fusion/api/lib.py | 20 ++++++++++++++++++-- openpype/hosts/fusion/api/pipeline.py | 22 ++++++---------------- openpype/hosts/fusion/api/workio.py | 2 +- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/fusion/api/__init__.py b/openpype/hosts/fusion/api/__init__.py index e5a7dac8c8..45ed4e12a3 100644 --- a/openpype/hosts/fusion/api/__init__.py +++ b/openpype/hosts/fusion/api/__init__.py @@ -5,10 +5,7 @@ from .pipeline import ( ls, imprint_container, - parse_container, - - get_current_comp, - comp_lock_and_undo_chunk + parse_container ) from .workio import ( @@ -24,7 +21,9 @@ from .lib import ( maintained_selection, get_additional_data, update_frame_range, - set_asset_framerange + set_asset_framerange, + get_current_comp, + comp_lock_and_undo_chunk ) from .menu import launch_openpype_menu @@ -39,9 +38,6 @@ __all__ = [ "imprint_container", "parse_container", - "get_current_comp", - "comp_lock_and_undo_chunk", - # workio "open_file", "save_file", @@ -55,6 +51,8 @@ __all__ = [ "get_additional_data", "update_frame_range", "set_asset_framerange", + "get_current_comp", + "comp_lock_and_undo_chunk", # menu "launch_openpype_menu", diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 91a74ac848..295ba33711 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -19,8 +19,6 @@ from openpype.pipeline import ( ) from openpype.pipeline.context_tools import get_current_project_asset -from .pipeline import get_current_comp, comp_lock_and_undo_chunk - self = sys.modules[__name__] self._project = None @@ -232,3 +230,21 @@ def get_frame_path(path): padding = 4 # default Fusion padding return filename, padding, ext + + +def get_current_comp(): + """Hack to get current comp in this session""" + fusion = getattr(sys.modules["__main__"], "fusion", None) + return fusion.CurrentComp if fusion else None + + +@contextlib.contextmanager +def comp_lock_and_undo_chunk(comp, undo_queue_name="Script CMD"): + """Lock comp and open an undo chunk during the context""" + try: + comp.Lock() + comp.StartUndo(undo_queue_name) + yield + finally: + comp.Unlock() + comp.EndUndo() \ No newline at end of file diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index b5d461e7f0..82cae427ff 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -23,6 +23,12 @@ from openpype.pipeline import ( ) from openpype.pipeline.load import any_outdated_containers from openpype.hosts.fusion import FUSION_HOST_DIR +from openpype.tools.utils import host_tools + +from .lib import ( + get_current_comp, + comp_lock_and_undo_chunk +) log = Logger.get_logger(__name__) @@ -247,19 +253,3 @@ def parse_container(tool): return container -def get_current_comp(): - """Hack to get current comp in this session""" - fusion = getattr(sys.modules["__main__"], "fusion", None) - return fusion.CurrentComp if fusion else None - - -@contextlib.contextmanager -def comp_lock_and_undo_chunk(comp, undo_queue_name="Script CMD"): - """Lock comp and open an undo chunk during the context""" - try: - comp.Lock() - comp.StartUndo(undo_queue_name) - yield - finally: - comp.Unlock() - comp.EndUndo() diff --git a/openpype/hosts/fusion/api/workio.py b/openpype/hosts/fusion/api/workio.py index 89752d3e6d..939b2ff4be 100644 --- a/openpype/hosts/fusion/api/workio.py +++ b/openpype/hosts/fusion/api/workio.py @@ -2,7 +2,7 @@ import sys import os -from .pipeline import get_current_comp +from .lib import get_current_comp def file_extensions(): From 9dda39a3e722e8a888f94de06072830cf241252d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 01:29:24 +0200 Subject: [PATCH 44/58] Remove unused imports --- openpype/hosts/fusion/api/pipeline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 82cae427ff..6d4a20ccee 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -2,9 +2,7 @@ Basic avalon integration """ import os -import sys import logging -import contextlib import pyblish.api From 15c3b068285fe2021d35822c32ad9d5b85e8574e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 01:36:31 +0200 Subject: [PATCH 45/58] Add validate comp prefs on scene before save and after scene open --- openpype/hosts/fusion/api/lib.py | 44 +++++++++++++++++++++++++++ openpype/hosts/fusion/api/pipeline.py | 9 +++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 295ba33711..f10809a7e1 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -5,6 +5,7 @@ import contextlib from Qt import QtGui +from openpype.lib import Logger from openpype.client import ( get_asset_by_name, get_subset_by_name, @@ -91,6 +92,49 @@ def set_asset_resolution(): }) +def validate_comp_prefs(): + """Validate current comp defaults with asset settings. + + Validates fps, resolutionWidth, resolutionHeight, aspectRatio. + + This does *not* validate frameStart, frameEnd, handleStart and handleEnd. + """ + + log = Logger.get_logger("validate_comp_prefs") + + fields = [ + "data.fps", + "data.resolutionWidth", + "data.resolutionHeight", + "data.pixelAspect" + ] + asset_data = get_current_project_asset(fields=fields)["data"] + + comp = get_current_comp() + comp_frame_format_prefs = comp.GetPrefs("Comp.FrameFormat") + + # Pixel aspect ratio in Fusion is set as AspectX and AspectY so we convert + # the data to something that is more sensible to Fusion + asset_data["pixelAspectX"] = asset_data.pop("pixelAspect") + asset_data["pixelAspectY"] = 1.0 + + for key, comp_key, label in [ + ("fps", "Rate", "FPS"), + ("resolutionWidth", "Width", "Resolution Width"), + ("resolutionHeight", "Height", "Resolution Height"), + ("pixelAspectX", "AspectX", "Pixel Aspect Ratio X"), + ("pixelAspectY", "AspectY", "Pixel Aspect Ratio Y") + ]: + value = asset_data[key] + current_value = comp_frame_format_prefs.get(comp_key) + if value != current_value: + # todo: Actually show dialog to user instead of just logging + log.warning( + "Invalid pref {}: {} (should be: {})".format(comp_key, + current_value, + value)) + + def get_additional_data(container): """Get Fusion related data for the container diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 6d4a20ccee..c2c39291c6 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -25,7 +25,8 @@ from openpype.tools.utils import host_tools from .lib import ( get_current_comp, - comp_lock_and_undo_chunk + comp_lock_and_undo_chunk, + validate_comp_prefs ) log = Logger.get_logger(__name__) @@ -88,6 +89,7 @@ def install(): # 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.save.before", on_before_save) register_event_callback("workfile.open.after", on_after_open) @@ -138,7 +140,12 @@ def on_pyblish_instance_toggled(instance, old_value, new_value): tool.SetAttrs({"TOOLB_PassThrough": passthrough}) +def on_before_save(_event): + validate_comp_prefs() + + def on_after_open(_event): + validate_comp_prefs() if any_outdated_containers(): log.warning("Scene has outdated content.") From fb940c5da099dd56cba7cbfedf9143e29e659b4a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 01:39:42 +0200 Subject: [PATCH 46/58] Don't error if "show" is clicked but comp is closed already --- openpype/hosts/fusion/api/pipeline.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index c2c39291c6..bc7f90a97c 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -156,7 +156,12 @@ def on_after_open(_event): comp = get_current_comp() def _on_show_scene_inventory(): - comp.CurrentFrame.ActivateFrame() # ensure that comp is active + # ensure that comp is active + frame = comp.CurrentFrame + if not frame: + print("Comp is closed, skipping show scene inventory") + return + frame.ActivateFrame() # raise comp window host_tools.show_scene_inventory() from openpype.widgets import popup From 1013a293519aed5470f1eda4e2ba4f280d718dba Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 01:41:04 +0200 Subject: [PATCH 47/58] Remove double space --- openpype/hosts/fusion/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index bc7f90a97c..083aa50027 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -167,7 +167,7 @@ def on_after_open(_event): from openpype.widgets import popup from openpype.style import load_stylesheet dialog = popup.Popup(parent=menu.menu) - dialog.setWindowTitle("Fusion comp has outdated content") + dialog.setWindowTitle("Fusion comp has outdated content") dialog.setMessage("There are outdated containers in " "your Fusion comp.") dialog.on_clicked.connect(_on_show_scene_inventory) From 2df5d871f16338a425870649a0df59d189acdaf4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 02:07:30 +0200 Subject: [PATCH 48/58] Remove before save callback since it runs BEFORE the task change making it unusable for these particular validations --- openpype/hosts/fusion/api/pipeline.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 083aa50027..76b365e29f 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -89,7 +89,6 @@ def install(): # 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.save.before", on_before_save) register_event_callback("workfile.open.after", on_after_open) @@ -140,10 +139,6 @@ def on_pyblish_instance_toggled(instance, old_value, new_value): tool.SetAttrs({"TOOLB_PassThrough": passthrough}) -def on_before_save(_event): - validate_comp_prefs() - - def on_after_open(_event): validate_comp_prefs() @@ -171,7 +166,6 @@ def on_after_open(_event): dialog.setMessage("There are outdated containers in " "your Fusion comp.") dialog.on_clicked.connect(_on_show_scene_inventory) - dialog.show() dialog.raise_() dialog.activateWindow() From 4c884ed2bc0649a5a6a08772fedd48ce06c1bda9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 02:08:09 +0200 Subject: [PATCH 49/58] Add a pop-up about invalid comp configuration --- openpype/hosts/fusion/api/lib.py | 58 +++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index f10809a7e1..a95312b938 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -103,12 +103,14 @@ def validate_comp_prefs(): log = Logger.get_logger("validate_comp_prefs") fields = [ + "name", "data.fps", "data.resolutionWidth", "data.resolutionHeight", "data.pixelAspect" ] - asset_data = get_current_project_asset(fields=fields)["data"] + asset_doc = get_current_project_asset(fields=fields) + asset_data = asset_doc["data"] comp = get_current_comp() comp_frame_format_prefs = comp.GetPrefs("Comp.FrameFormat") @@ -118,21 +120,59 @@ def validate_comp_prefs(): asset_data["pixelAspectX"] = asset_data.pop("pixelAspect") asset_data["pixelAspectY"] = 1.0 - for key, comp_key, label in [ + validations = [ ("fps", "Rate", "FPS"), ("resolutionWidth", "Width", "Resolution Width"), ("resolutionHeight", "Height", "Resolution Height"), ("pixelAspectX", "AspectX", "Pixel Aspect Ratio X"), ("pixelAspectY", "AspectY", "Pixel Aspect Ratio Y") - ]: - value = asset_data[key] - current_value = comp_frame_format_prefs.get(comp_key) - if value != current_value: + ] + + invalid = [] + for key, comp_key, label in validations: + asset_value = asset_data[key] + comp_value = comp_frame_format_prefs.get(comp_key) + if asset_value != comp_value: # todo: Actually show dialog to user instead of just logging log.warning( - "Invalid pref {}: {} (should be: {})".format(comp_key, - current_value, - value)) + "Comp {pref} {value} does not match asset " + "'{asset_name}' {pref} {asset_value}".format( + pref=label, + value=comp_value, + asset_name=asset_doc["name"], + asset_value=asset_value) + ) + + invalid_msg = "{} {} should be {}".format(label, + comp_value, + asset_value) + invalid.append(invalid_msg) + + if invalid: + + def _on_repair(): + attributes = dict() + for key, comp_key, _label in validations: + value = asset_data[key] + comp_key_full = "Comp.FrameFormat.{}".format(comp_key) + attributes[comp_key_full] = value + comp.SetPrefs(attributes) + + from . import menu + from openpype.widgets import popup + from openpype.style import load_stylesheet + dialog = popup.Popup(parent=menu.menu) + dialog.setWindowTitle("Fusion comp has invalid configuration") + + msg = "Comp preferences mismatches '{}'".format(asset_doc["name"]) + msg += "\n" + "\n".join(invalid) + dialog.setMessage(msg) + dialog.setButtonText("Repair") + dialog.on_clicked.connect(_on_repair) + dialog.show() + dialog.raise_() + dialog.activateWindow() + dialog.setStyleSheet(load_stylesheet()) def get_additional_data(container): From 794e8a9b8504ccf99d0795d54b7ab9ec3feb6a78 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 08:35:23 +0200 Subject: [PATCH 50/58] Shush hound --- openpype/hosts/fusion/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index a95312b938..a7472d239c 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -331,4 +331,4 @@ def comp_lock_and_undo_chunk(comp, undo_queue_name="Script CMD"): yield finally: comp.Unlock() - comp.EndUndo() \ No newline at end of file + comp.EndUndo() From 64ef00b564eaccc0cad74e4c59e318724f4a8315 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 16:01:26 +0200 Subject: [PATCH 51/58] Set OpenPype icon for menu (cherry picked from commit c902d7b8130ae21b1808e54643b1de59a54f5c43) --- openpype/hosts/fusion/api/menu.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index cf3dea8ec3..ba0e267b14 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -1,6 +1,6 @@ import sys -from Qt import QtWidgets, QtCore +from Qt import QtWidgets, QtCore, QtGui from openpype.tools.utils import host_tools from openpype.style import load_stylesheet @@ -14,6 +14,7 @@ from openpype.hosts.fusion.api.lib import ( set_asset_resolution ) from openpype.pipeline import legacy_io +from openpype.resources import get_openpype_icon_filepath from .pulse import FusionPulse @@ -44,6 +45,10 @@ class OpenPypeMenu(QtWidgets.QWidget): self.setObjectName("OpenPypeMenu") + icon_path = get_openpype_icon_filepath() + icon = QtGui.QIcon(icon_path) + self.setWindowIcon(icon) + self.setWindowFlags( QtCore.Qt.Window | QtCore.Qt.CustomizeWindowHint From a8d90473b8e9bf331a3ff740095bc843c17afb2a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 19:42:21 +0200 Subject: [PATCH 52/58] Remove redundant Spacer widget and use `QVBoxLayout.addSpacing` --- openpype/hosts/fusion/api/menu.py | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index ba0e267b14..949b905705 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -22,23 +22,6 @@ self = sys.modules[__name__] self.menu = None -class Spacer(QtWidgets.QWidget): - def __init__(self, height, *args, **kwargs): - super(Spacer, self).__init__(*args, **kwargs) - - self.setFixedHeight(height) - - real_spacer = QtWidgets.QWidget(self) - real_spacer.setObjectName("Spacer") - real_spacer.setFixedHeight(height) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(real_spacer) - - self.setLayout(layout) - - class OpenPypeMenu(QtWidgets.QWidget): def __init__(self, *args, **kwargs): super(OpenPypeMenu, self).__init__(*args, **kwargs) @@ -86,28 +69,28 @@ class OpenPypeMenu(QtWidgets.QWidget): layout.addWidget(asset_label) - layout.addWidget(Spacer(15, self)) + layout.addSpacing(20) layout.addWidget(workfiles_btn) - layout.addWidget(Spacer(15, self)) + layout.addSpacing(20) layout.addWidget(create_btn) layout.addWidget(load_btn) layout.addWidget(publish_btn) layout.addWidget(manager_btn) - layout.addWidget(Spacer(15, self)) + layout.addSpacing(20) layout.addWidget(libload_btn) - layout.addWidget(Spacer(15, self)) + layout.addSpacing(20) layout.addWidget(set_framerange_btn) layout.addWidget(set_resolution_btn) layout.addWidget(rendermode_btn) - layout.addWidget(Spacer(15, self)) + layout.addSpacing(20) layout.addWidget(duplicate_with_inputs_btn) From 1ce7e697ec2b64f135113e8e3c8832c2ada33972 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 19:43:56 +0200 Subject: [PATCH 53/58] Remove "Clicked {button}" print statements - UIs have pretty much no delays so no need to print --- openpype/hosts/fusion/api/menu.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 949b905705..7a6293807f 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -138,31 +138,24 @@ class OpenPypeMenu(QtWidgets.QWidget): self._callbacks[:] = [] def on_workfile_clicked(self): - print("Clicked Workfile") host_tools.show_workfiles() def on_create_clicked(self): - print("Clicked Create") host_tools.show_creator() def on_publish_clicked(self): - print("Clicked Publish") host_tools.show_publish() def on_load_clicked(self): - print("Clicked Load") host_tools.show_loader(use_context=True) def on_manager_clicked(self): - print("Clicked Manager") host_tools.show_scene_inventory() def on_libload_clicked(self): - print("Clicked Library") host_tools.show_library_loader() def on_rendermode_clicked(self): - print("Clicked Set Render Mode") if self.render_mode_widget is None: window = set_rendermode.SetRenderMode() window.setStyleSheet(load_stylesheet()) @@ -172,15 +165,12 @@ class OpenPypeMenu(QtWidgets.QWidget): self.render_mode_widget.show() def on_duplicate_with_inputs_clicked(self): - print("Clicked Duplicate with input connections") duplicate_with_inputs.duplicate_with_input_connections() def on_set_resolution_clicked(self): - print("Clicked Reset Resolution") set_asset_resolution() def on_set_framerange_clicked(self): - print("Clicked Reset Framerange") set_asset_framerange() From 15610328be6f94ed00aaaa166062438bc8bda3f6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 19:46:13 +0200 Subject: [PATCH 54/58] Get current comp once and as early as possible --- openpype/hosts/fusion/api/lib.py | 6 ++++-- openpype/hosts/fusion/api/pipeline.py | 5 ++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index a7472d239c..956f3557ad 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -92,7 +92,7 @@ def set_asset_resolution(): }) -def validate_comp_prefs(): +def validate_comp_prefs(comp=None): """Validate current comp defaults with asset settings. Validates fps, resolutionWidth, resolutionHeight, aspectRatio. @@ -100,6 +100,9 @@ def validate_comp_prefs(): This does *not* validate frameStart, frameEnd, handleStart and handleEnd. """ + if comp is None: + comp = get_current_comp() + log = Logger.get_logger("validate_comp_prefs") fields = [ @@ -112,7 +115,6 @@ def validate_comp_prefs(): asset_doc = get_current_project_asset(fields=fields) asset_data = asset_doc["data"] - comp = get_current_comp() comp_frame_format_prefs = comp.GetPrefs("Comp.FrameFormat") # Pixel aspect ratio in Fusion is set as AspectX and AspectY so we convert diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 76b365e29f..7933f160dc 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -140,7 +140,8 @@ def on_pyblish_instance_toggled(instance, old_value, new_value): def on_after_open(_event): - validate_comp_prefs() + comp = get_current_comp() + validate_comp_prefs(comp) if any_outdated_containers(): log.warning("Scene has outdated content.") @@ -148,8 +149,6 @@ def on_after_open(_event): # Find OpenPype menu to attach to from . import menu - comp = get_current_comp() - def _on_show_scene_inventory(): # ensure that comp is active frame = comp.CurrentFrame From 6bcb9dd2471cf39dc21f2ea71e05cdfe4fea650c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 20:01:53 +0200 Subject: [PATCH 55/58] Remove `get_additional_data` OpenPype manager/containers don't use it so it's redundant in the code base. --- openpype/hosts/fusion/api/__init__.py | 2 -- openpype/hosts/fusion/api/lib.py | 20 -------------------- 2 files changed, 22 deletions(-) diff --git a/openpype/hosts/fusion/api/__init__.py b/openpype/hosts/fusion/api/__init__.py index 45ed4e12a3..ed70dbca50 100644 --- a/openpype/hosts/fusion/api/__init__.py +++ b/openpype/hosts/fusion/api/__init__.py @@ -19,7 +19,6 @@ from .workio import ( from .lib import ( maintained_selection, - get_additional_data, update_frame_range, set_asset_framerange, get_current_comp, @@ -48,7 +47,6 @@ __all__ = [ # lib "maintained_selection", - "get_additional_data", "update_frame_range", "set_asset_framerange", "get_current_comp", diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 956f3557ad..4ef44dbb61 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -177,26 +177,6 @@ def validate_comp_prefs(comp=None): dialog.setStyleSheet(load_stylesheet()) -def get_additional_data(container): - """Get Fusion related data for the container - - Args: - container(dict): the container found by the ls() function - - Returns: - dict - """ - - tool = container["_tool"] - tile_color = tool.TileColor - if tile_color is None: - return {} - - return {"color": QtGui.QColor.fromRgbF(tile_color["R"], - tile_color["G"], - tile_color["B"])} - - def switch_item(container, asset_name=None, subset_name=None, From 3c134ec011ae2b8ffecda43f1281b77a59d591bc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 20:05:15 +0200 Subject: [PATCH 56/58] Remove redundant saying "installed" even though it's midway during install + fix comment --- openpype/hosts/fusion/api/pipeline.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 7933f160dc..260c7d9e60 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -60,7 +60,7 @@ def install(): """ # Remove all handlers associated with the root logger object, because - # that one sometimes logs as "warnings" incorrectly. + # that one always logs as "warnings" incorrectly. for handler in logging.root.handlers[:]: logging.root.removeHandler(handler) @@ -72,8 +72,6 @@ def install(): logger.addHandler(handler) logger.setLevel(logging.DEBUG) - log.info("openpype.hosts.fusion installed") - pyblish.api.register_host("fusion") pyblish.api.register_plugin_path(PUBLISH_PATH) log.info("Registering Fusion plug-ins..") From 0b525fa3658c926be033afda4462b4769b1aa025 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 20:17:33 +0200 Subject: [PATCH 57/58] Cleanup Create EXR saver logic --- .../fusion/plugins/create/create_exr_saver.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_exr_saver.py b/openpype/hosts/fusion/plugins/create/create_exr_saver.py index 8bab5ee9b1..6d93fe710a 100644 --- a/openpype/hosts/fusion/plugins/create/create_exr_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_exr_saver.py @@ -1,6 +1,9 @@ import os -from openpype.pipeline import LegacyCreator +from openpype.pipeline import ( + LegacyCreator, + legacy_io +) from openpype.hosts.fusion.api import ( get_current_comp, comp_lock_and_undo_chunk @@ -21,12 +24,9 @@ class CreateOpenEXRSaver(LegacyCreator): comp = get_current_comp() - # todo: improve method of getting current environment - # todo: pref avalon.Session over os.environ + workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) - workdir = os.path.normpath(os.environ["AVALON_WORKDIR"]) - - filename = "{}..tiff".format(self.name) + filename = "{}..exr".format(self.name) filepath = os.path.join(workdir, "render", filename) with comp_lock_and_undo_chunk(comp): @@ -39,10 +39,10 @@ class CreateOpenEXRSaver(LegacyCreator): saver["Clip"] = filepath saver["OutputFormat"] = file_format - # # # Set standard TIFF settings + # Check file format settings are available if saver[file_format] is None: - raise RuntimeError("File format is not set to TiffFormat, " - "this is a bug") + raise RuntimeError("File format is not set to {}, " + "this is a bug".format(file_format)) # Set file format attributes saver[file_format]["Depth"] = 1 # int8 | int16 | float32 | other From b62d12779a67a266a3109af0433eb234397200a0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 20:17:52 +0200 Subject: [PATCH 58/58] Clean up docstring --- openpype/hosts/fusion/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 260c7d9e60..c92d072ef7 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -48,7 +48,7 @@ class CompLogHandler(logging.Handler): def install(): - """Install fusion-specific functionality of avalon-core. + """Install fusion-specific functionality of OpenPype. This is where you install menus and register families, data and loaders into fusion.